Контроль состояния извне StatefulWidget

Я пытаюсь понять лучшую практику для контроля состояния StatefulWidget за пределами этого состояния Widgets.

У меня определен следующий интерфейс.

abstract class StartupView {
  Stream<String> get onAppSelected;

  set showActivity(bool activity);
  set message(String message);
}

Я хотел бы создать StatefulWidget StartupPage который реализует этот интерфейс. Я ожидаю, что виджет будет делать следующее:

  1. Когда кнопка нажата, она отправит событие через поток onAppSelected. Контроллер прослушивает это даже и выполняет некоторые действия (вызов БД, запрос на обслуживание и т. Д.).

  2. Контроллер может позвонить showActivity или же set message чтобы представление показывало прогресс с сообщением.

Поскольку виджет Stateful не отображает свое состояние как свойство, я не знаю лучшего подхода для доступа и изменения атрибутов State.

То, как я ожидал бы использовать это было бы что-то вроде:

Widget createStartupPage() {
    var page = new StartupPage();
    page.onAppSelected.listen((app) {
      page.showActivity = true;
      //Do some work
      page.showActivity = false;
    });
  }

Я думал о создании экземпляра виджета, передавая состояние, которое я хочу вернуть в createState() но это чувствует себя неправильно.

Некоторые сведения о том, почему у нас такой подход: в настоящее время у нас есть веб-приложение Dart. Для разделения контроллера представления, тестируемости и дальновидности в отношении Flutter мы решили, что создадим интерфейс для каждого представления в нашем приложении. Это позволит WebComponent или Flidter Widget реализовать этот интерфейс и оставить всю логику контроллера одинаковой.

8 ответов

Решение

Вы можете открыть виджет состояния статическим методом, несколько примеров трепетания делают это таким образом, и я тоже начал его использовать:

class StartupPage extends StatefulWidget {
  static _StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<_StartupPageState>());

  @override
  _StartupPageState createState() => new _StartupPageState();
}

class _StartupPageState extends State<StartupPage> {
  ...
}

Затем вы можете получить доступ к состоянию, позвонив StartupPage.of(context).doSomething();,

Предостережение в том, что вам нужно иметь BuildContext с этой страницей где-то в ее дереве.

Существует несколько способов взаимодействия с другими виджетами с сохранением состояния.

1. ancestorStateOfType

Первый и самый простой через context.ancestorStateOfType метод.

Обычно завернутый в статический метод Stateful подкласс, как это:

class MyState extends StatefulWidget {
  static of(BuildContext context, {bool root = false}) => root
      ? context.rootAncestorStateOfType(const TypeMatcher<_MyStateState>())
      : context.ancestorStateOfType(const TypeMatcher<_MyStateState>());

  @override
  _MyStateState createState() => _MyStateState();
}

class _MyStateState extends State<MyState> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Вот как Navigator работает например.

Pro:

  • Самое простое решение

Против:

  • Соблазн для доступа State свойства или вызов вручную setState
  • Требуется выставить State подкласс

Не используйте этот метод, если вы хотите получить доступ к переменной. Так как ваш виджет может не перезагрузиться при изменении этой переменной.

2. Прослушиваемый, потоковый и / или унаследованный виджет

Иногда вместо метода вы можете получить доступ к некоторым свойствам. Дело в том, что вы, скорее всего, хотите, чтобы ваши виджеты обновлялись всякий раз, когда это значение меняется со временем.

В этой ситуации дартс предлагают Stream а также Sink, И трепетание добавляет сверху InheritedWidget а также Listenable такие как ValueNotifier, Все они делают относительно одно и то же: подписываются на событие изменения значения в сочетании с StreamBuilder/context.inheritFromWidgetOfExactType/AnimatedBuilder,

Это подходящее решение, когда вы хотите State выставить некоторые свойства. Я не буду охватывать все возможности, но вот небольшой пример использования InheritedWidget:

Во-первых, у нас есть InheritedWidget которые разоблачают count:

class Count extends InheritedWidget {
  static of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(Count);

  final int count;

  Count({Key key, @required Widget child, @required this.count})
      : assert(count != null),
        super(key: key, child: child);

  @override
  bool updateShouldNotify(Count oldWidget) {
    return this.count != oldWidget.count;
  }
}

Тогда у нас есть State которые создают это InheritedWidget

class _MyStateState extends State<MyState> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Count(
      count: count,
      child: Scaffold(
        body: CountBody(),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
        ),
      ),
    );
  }
}

Наконец, у нас есть CountBody что принести это выставлено count

class CountBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(Count.of(context).count.toString()),
    );
  }
}

Плюсы:

  • Более производительный, чем ancestorStateOfType
  • Потоковая альтернатива - только дротик (работает с сетью) и сильно интегрирована в язык (ключевые слова, такие как await for или же async*)
  • Автономная перезагрузка детей при изменении значения

Минусы:

  • Больше шаблонов
  • Поток может быть сложным

3. Уведомления

Вместо прямого вызова методов на StateВы можете отправить Notification от вашего виджета. И сделать State подписаться на эти уведомления.

Пример Notification было бы:

class MyNotification extends Notification {
  final String title;

  const MyNotification({this.title});
}

Чтобы отправить уведомление просто позвоните dispatch(context) на вашем экземпляре уведомления, и он будет пузыриться.

MyNotification(title: "Foo")..dispatch(context)

Любой данный виджет может прослушивать уведомления, отправленные их детьми с помощью NotificationListener<T>:

class _MyStateState extends State<MyState> {
  @override
  Widget build(BuildContext context) {
    return NotificationListener<MyNotification>(
      onNotification: onTitlePush,
      child: Container(),
    );
  }

  void onTitlePush(MyNotification notification) {
    print("New item ${notification.title}");
  }
}

Примером будет Scrollable, который может отправить ScrollNotification в том числе начало / конец / прокрутка. Затем используется Scrollbar знать информацию прокрутки, не имея доступа к ScrollController

Плюсы:

  • Классный реактивный API. Мы не занимаемся напрямую State, Это State который подписывается на события, вызванные его детьми
  • Более одного виджета могут подписаться на одно и то же уведомление
  • Предотвращает доступ детей к нежелательным State свойства

Минусы:

  • Может не подходить вашему варианту использования
  • Требуется больше шаблона

Существует еще один распространенный подход к получению доступа к свойствам / методам State:

class StartupPage extends StatefulWidget {
  StartupPage({Key key}) : super(key: key);

  @override
  StartupPageState createState() => StartupPageState();
}

// Make class public!
class StartupPageState extends State<StartupPage> {
  int someStateProperty;

  void someStateMethod() {}
}

// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();

// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);

// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();

Я делаю:

class StartupPage extends StatefulWidget {
  StartupPageState state;

  @override
  StartupPageState createState() {
    this.state = new StartupPageState();

    return this.state;
  }
}

class DetectedAnimationState extends State<DetectedAnimation> {

И снаружи просто startupPage.state

Пытаясь решить аналогичную проблему, я обнаружил, что ancestorStateOfType() а также TypeMatcherустарели. Вместо этого нужно использоватьfindAncestorStateOfType(). Однако, согласно документации, "вызов этого метода относительно дорогостоящий". Документация дляfindAncestorStateOfType()метод можно найти здесь.

В любом случае использовать findAncestorStateOfType(), может быть реализовано следующее (это модификация правильного ответа с помощью findAncestorStateOfType() метод):

class StartupPage extends StatefulWidget {
  static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();

  @override
  _StartupPageState createState() => new _StartupPageState();
}

class _StartupPageState extends State<StartupPage> {
  ...
}

Доступ к состоянию можно получить так же, как описано в правильном ответе (используя StartupPage.of(context).yourFunction()). Я хотел обновить пост новым методом.

Вы можете использовать eventify

Эта библиотека предоставляет механизм для регистрации уведомлений о событиях у отправителя или издателя и получения уведомлений в случае события.

Вы можете сделать что-то вроде:

// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();

var controlNumber = 50;

List<Widget> buttonsGenerator() {
    final List<Widget> buttons = new List<Widget>();
    for (var i = 0; i < controlNumber; i++) {
        widgets.add(new MaterialButton(
                  // Generate 10 Buttons afterwards
                  onPressed: () {
                    controlNumber = 10;
                    emitter.emit("updateButtonsList", null, "");
                  },
                );
    }
}

class AState extends State<ofYourWidget> {
    @override
    Widget build(BuildContext context) {
        List<Widget> buttons_list = buttonsGenerator();
        emitter.on('updateButtonsList', null, (event, event_context) {
            setState(() {
                buttons_list = buttonsGenerator();
            });
        });
    }
    ...
}

Я не могу придумать ничего, чего нельзя было бы достичь с помощью событийного программирования. Вы безграничны!

"Свободу нельзя дать - ее нужно достичь". - Эльберт Хаббард

Рассматривали ли вы поднять состояние родительского виджета? Насколько мне известно, это общий, хотя и менее идеальный, чем Redux, способ управления состоянием в React, и этот репозиторий показывает, как применить концепцию к приложению Flutter.

Для кого-то еще, кому может понадобиться это решение. Вот вклад в ответ @santi:

      class StartupPage extends StatefulWidget {
  final StartupPageState _state = StartupPageState();

  @override
  StartupPageState createState() {
     return _state;
  }
}

Это гарантирует, что один и тот же экземпляр _state будет использоваться для каждого вызова createState(). Я решил сделать приватным, чтобы вы не открывали все его атрибуты внешним классам. Если вы хотите использовать конкретное поле из него, я бы посоветовал вам использовать инкапсуляцию для доступа / изменения его (используя Переменная).

Другие вопросы по тегам