Управление состоянием за пределами StatefulWidget

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

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

abstract class StartupView {
  Stream<String> get onAppSelected;

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

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

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

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

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

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

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

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

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

Ответ 1

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

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 с этой страницей где-то в своем дереве.

Ответ 2

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

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 который 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(),
    );
  }

  bool onTitlePush(MyNotification notification) {
    print("New item ${notification.title}");
    // true meaning processed, no following notification bubbling.
    return true;
  }
}

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

Плюсы:

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

Минусы:

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

Ответ 3

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