Управление состоянием за пределами StatefulWidget
Я пытаюсь понять наилучшую практику для управления состоянием StatefulWidget за пределами этого состояния Widgets.
У меня есть следующий интерфейс.
abstract class StartupView {
Stream<String> get onAppSelected;
set showActivity(bool activity);
set message(String message);
}
Я хотел бы создать StatefulWidget StartupPage
который реализует этот интерфейс. Я ожидаю, что Widget сделает следующее:
-
Когда кнопка нажата, она отправляет событие поверх потока onAppSelected. Контроллер будет слушать это ровно и выполнять некоторые действия (вызов db, запрос службы и т.д.).
-
Контроллер может вызвать 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.