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