Этот пост поможет вам глубже понять механизм работы состояний в Flutter и через анализ библиотеки управления состояниями Provider сделать это еще более понятным. После прочтения этой статьи вы сможете легче осмыслить ваш "гнездо состояний".
⚠️ В двенадцатой статье больше внимания уделяется анализу библиотек управления состоянием, а эта статья больше сосредоточена на внутренней реализации состояний в Flutter.
Мы знаем, что во вселенной Flutter все объекты являются Widget
, а Widget
является @immutable, то есть неизменяемым, поэтому каждый Widget
представляет собой отдельную кадровую картинку.
На этом основании, StatefulWidget
использует State
для реализации перерисовки между кадрами, то есть при каждом перерисовывании Widget
, State
предоставляет новые данные для перерисовки.
Для этого важно понять принцип реализации Widget
в Flutter, который мы уже рассматривали ранее. Здесь стоит обратить внимание на два ключевых понятия:
В Flutter Widget
обычно преобразуется в RenderObject
через Element
для отрисовки.
Element
- это реализация BuildContext
, которая также хранит RenderObject
и Widget
. **Метод Widget build(BuildContext context)
вызывается Element
'ом.**Зная эти концепции, рассмотрим следующий рисунок. При создании Widget
в Flutter сначала создаётся его Element
. Передача состояния между кадрами осуществляется тем, что State
сохраняется внутри Element
. Таким образом, когда Element
вызывает метод Widget build()
, он получает новый Widget
через state.build(this)
, благодаря чему данные, хранящиеся в State
, могут быть переиспользованы.
Где же создаётся State
?
Как показано на следующем рисунке, метод createState
в StatefulWidget
создаётся в методе создания StatefulElement
. Это гарантирует, что если Element
не будет пересоздан, State
будет постоянно переиспользоваться.
Кроме того, рассмотрим метод update
: когда новый StatefulWidget
создаётся для обновления UI, новый widget
присваивается _state
, что может привести к часто игнорируемому новичками вопросу.
Давайте сначала рассмотрим проблемный код, как показано на следующем рисунке:
_DemoAppState
мы создали DemoPage
, передав ему переменную data
.createState
в DemoPage
переменная data
была передана непосредственно в _DemoPageState
._DemoPageState
переданная переменная data
отображается через компонент Text
.После запуска программы ничего особенного не происходит, верно? Но когда мы кликнем по кнопке "setState" в пункте 4, оказывается, что текст в пункте 3 не меняется, почему это происходит?
Проблема заключается в методах создания (build
) и обновления (update
) объекта StatefulElement
:
Объект State
создаётся только при вызове метода build
объекта StatefulElement
. Когда мы вызываем setState
, который приводит к выполнению метода update
, выполняется только _state.widget = newWidget
. Переданная переменная data
через _DemoPageState(this.data)
остаётся неизменной после вызова setState
.
Если бы мы использовали метод widget.data
, как указано в примечании 3 выше, то поскольку _state.widget = newWidget
обновляет Widget
внутри State
, компонент Text
будет автоматически обновлён.
setState
?Часто упоминаемый setState
фактически вызывает метод markNeedsBuild
. Метод markNeedsBuild
помечает element
как dirty
, а затем он будет перерисован в следующей кадровой рамке (WidgetsBinding.drawFrame
). Это также указывает на то, что setState
не действует сразу.
Ранее мы говорили о роли и принципах работы объекта State
в Flutter. Теперь давайте поговорим о старом знакомом — InheritedWidget
.
Поделиться состоянием является распространенной задачей, например, информация пользователя и состояние входа. В Flutter объект InheritedWidget
был специально создан для этой цели. Мы уже кратко затронули его в двенадцатой статье:> Внутри объекта Element
есть параметр Map<Type, InheritedElement> _inheritedWidgets;
. Обычно этот параметр пуст, но он инициализируется только тогда, когда родительский компонент является InheritedWidget
или сам является InheritedWidgets
. Этот Map
передается и объединяется уровнем ниже, если родительский компонент является InheritedWidget
.
Поэтому, когда мы используем
context
для вызоваinheritFromWidgetOfExactType
, мы можем использовать этотMap
для поиска родительскогоInheritedWidget
. Конечно, это перевод:О да,InheritedWidget
делится именноWidget
, но этотWidget
являетсяProxyWidget
, который сам по себе ничего не отрисовывает, однако передача значений, хранящихся внутри этогоWidget
, позволяет достичь цели совместного использования состояния.
Как показано ниже, в Flutter совместное использование Theme
происходит за счет разделения _InheritedTheme
какого-то Widget
, а то, что мы получаем с помощью Theme.of(context)
, это просто данные ThemeData
, хранящиеся внутри этого Widget
.
static ThemeData of(BuildContext context, {bool shadowThemeOnly = false}) {
final _InheritedTheme inheritedTheme = context.inheritFromWidgetOfExactType(_InheritedTheme);
if (shadowThemeOnly) {
// тема внутри этого виджета
// тема содержит нужные нам данные ThemeData
return inheritedTheme.theme.data;
}
...
}
Важно отметить, что делает метод inheritFromWidgetOfExactType
?
Прямым образом найдите реализацию метода inheritFromWidgetOfExactType
в классе Element
, как показано ниже ключевыми строками кода:
InheritedElement
в карте _inheritedWidgets
._dependencies
, и через вызов метода updateDependencies
текущий Element
добавляется в карту _dependents
внутри InheritedElement
.Widget
из InheritedElement
.```dart
@Override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, {Object aspect}) {
// поиск в карте _inheritedWidgets
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
// возврат найденного InheritedWidget, при этом добавление текущего элемента
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}@override
void уведомитьКлиентов(InheritedWidget старыйВиджет) {
for (Элемент dependent in _зависимые.keys) {
уведомитьЗависимого(старыйВиджет, dependent);
}
}
```
Ключевой момент здесь заключается в методе **`ancestor.updateDependencies(this, aspect)`**:
Как известно, получение `InheritedWidget` обычно требует `BuildContext`, как в случае с `Theme.of(context)`. Реализация `BuildContext` — это `Element`. **Поэтому при вызове `context.inheritFromWidgetOfExactType` этот `context` представляющий `Element` добавляется в `_dependents` объекта `InheritedElement`.**
*Что это значит?*
Например, когда мы вызываем `Theme.of(context).primaryColor` в `StatefulWidget`, **переданный `context` представляет собой `Element` этого `Widget`, который "зарегистрирован" в `_dependents` объекта `InheritedElement`.**
**И когда `InheritedWidget` обновляется, как показано ниже, каждый `Element` в `_dependents` вызывает метод `notifyDependent`, что приводит к выполнению `markNeedsBuild`.** Это объясняет, почему места, где используется `Theme.of(context).primaryColor`, также обновляются при обновлении `InheritedWidget`.

> Дальше начинается реальная аналитика **Provider**.
## Второй раздел: Provider
*Почему существует **Provider**?*
```Исходя из схожести технологической стека Flutter и React, в Flutter появились такие библиотеки для управления состоянием, как `flutter_redux`, `flutter_dva`, `flutter_mobx`, `fish_flutter` и другие, которые часто используются в фронтенд-разработке. Большинство этих решений довольно сложны и требуют понимания концепций фреймворка.
А поскольку официально рекомендованное управление состоянием от Flutter, `scoped_model`, имеет простую архитектуру, иногда он не подходит для сложных случаев.
Поэтому после некоторого периода экспериментов и проб и ошибок, **после Google I/O конференции, [Provider](https://github.com/rrousselGit/provider) стал одним из новых официальных способов управления состоянием в Flutter.**
Основные характеристики **Provider**: **не сложность, легкость понимания, небольшое количество кода, удобство комбинирования и контроля над частичной перезагрузкой**. Официальный стейт-менеджмент от Google, [flutter_provide](https://github.com/google/flutter-provide), был прекращен, и [provider](https://github.com/rrousselGit/provider) стал его заменителем.```
⚠️ Обратите внимание, что `provider` отличается от `flutter-provide` наличием буквы `r`.
```> Отступление: во время собеседований меня иногда спрашивали "количество вашего открытого проекта тоже невелико", на что я обычно отвечал с улыбкой, **хотя количество кода может указывать на некоторые достижения, но я категорически против использования количества кода как меры вклада, это то же самое, что использовать продолжительность работы для оценки ценности сотрудника?**
### 0. Демонстрационный код
Ниже представлен код, который реализует счетчик кликов. В нём:
- В классе `_ProviderPageState` используется `MultiProvider`, чтобы предоставить несколько провайдеров.
- В классе `CountWidget` через `Consumer` получается значение `counter`, которое одновременно обновляет `AppBar` в `_ProviderPageState` и текстовое поле в `CountWidget`.
```dart
class _ProviderPageState extends State<ProviderPage> {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => ProviderModel()),
],
child: Scaffold(
appBar: AppBar(
title: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
var counter = Provider.of<ProviderModel>(context);
return Text("Provider ${counter.count.toString()}");
},
),
),
body: CountWidget(),
),
);
}
}
class CountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ProviderModel>(
builder: (context, counter, _) {
return Column(
children: <Widget>[
Expanded(
child: Center(
child: Text(counter.count.toString()),
),
),
Center(
child: FlatButton(
onPressed: () {
counter.add();
},
color: Colors.blue,
child: Text("+"),
),
),
],
);
},
);
}
}
class ProviderModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void add() {
_count++;
notifyListeners();
}
}
```
Поэтому в вышеприведённом коде мы используем `ChangeNotifierProvider` вместе с `ChangeNotifier` (ProviderModel) для обеспечения совместного доступа; используем `Provider.of` и `Consumer` для получения состояния `counter`; а также вызываем метод `notifyListeners()` класса `ChangeNotifier` для обновления данных. Вот несколько ключевых моментов:- 1. Внутренний компонент `DelegateWidget` в **Provider** является `StatefulWidget`, поэтому он может обновляться и иметь жизненный цикл.
- 2. Общие состояния реализуются с помощью `InheritedProvider`, который является подклассом `InheritedWidget`.
- 3. Сочетание `MultiProvider` и `Consumer` позволяет контролировать составление и частичное обновление.
Далее мы рассмотрим каждый пункт подробнее.
### 1. Delegate
Если это управление состоянием, то обязательно должны присутствовать `StatefulWidget` и вызов метода `setState`.
В **Provider**, все операции управления жизненным циклом и обновления `StatefulWidget` осуществляются через различные агенты, как показано ниже. Например, `ChangeNotifierProvider` проходит следующий процесс:
- Установка `ChangeNotifier` в `ChangeNotifierProvider` выполняется с использованием метода `addListener`, чтобы добавить слушатель событий.
- Внутри слушателя событий вызывается метод `StateSetter` объекта `StateDelegate`, что приводит к вызову `setState` у `StatefulWidget`.
- При выполнении метода `notifyListeners` у `ChangeNotifier` происходит конечный вызов `setState`, что приводит к обновлению.

Используемый нами `MultiProvider` позволяет нам объединять несколько `Provider`. Как показано ниже, переданные `providers` располагаются в обратном порядке, создавая в результате вложенные деревья виджетов, что удобно для добавления нескольких `Provider`:```dart
@override
Widget build(BuildContext context) {
var tree = child;
for (final provider in providers.reversed) {
tree = provider.cloneWithChild(tree);
}
return tree;
}
/// Клонирует текущего провайдера с новым [child].
/// Примечание для реализаторов: все остальные значения, включая [Key], должны быть сохранены.
@override
MultiProvider cloneWithChild(Widget child) {
return MultiProvider(
key: key,
providers: providers,
child: child,
);
}
```
Через различные этапы жизненного цикла, такие как `Disposer`, также можно использовать внешние вторичные обработчики, что помогает минимизировать использование вложенных `StatefulWidget`.
### 2. InheritedProvider
Общий доступ к состоянию требует использования `InheritedWidget`. `InheritedProvider` является подклассом `InheritedWidget`, и все реализации `Provider` используют `InheritedProvider` внутри метода `build` для обеспечения совместного использования значений.
### 3. Consumer
`Consumer` — это интересный элемент в `Provider`, который представляет собой `StatelessWidget`. В методе `build` он использует `Provider.of<T>(context)` для получения значения `T`, которое было установлено через `InheritedWidget`.
```dart
final Widget Function(BuildContext context, T value, Widget child) builder;
@override
Widget build(BuildContext context) {
return builder(context, Provider.of<T>(context), child);
}
```
Можно ли использовать `Provider.of<T>(context)` напрямую, а не через `Consumer`?
Конечно, можно. Однако помните, что при использовании `InheritedWidget` контекст `context` регистрируется в `_dependents` `InheritedElement`.
```Как отдельный `StatelessWidget`, `Consumer` имеет преимущество в том, что `context`, передаваемый в `Provider.of<T>(context)`, относится именно к самому `Consumer`. Это позволяет ограничить обновление до конкретного `Consumer`, когда значение `InheritedWidget` меняется, вместо того чтобы обновлять весь экран.
**Поэтому `Consumer` удобно упаковывает логику регистрации `context` в `InheritedWidget`, тем самым контролируя степень детализации при обновлении состояния.**
Кроме того, библиотека предоставляет комбинированные версии `Consumer2`–`Consumer6`:
```
@override
Widget build(BuildContext context) {
return builder(
context,
Provider.of<A>(context),
Provider.of<B>(context),
Provider.of<C>(context),
Provider.of<D>(context),
Provider.of<E>(context),
Provider.of<F>(context),
child,
);
}
```
Эта конфигурация должна понравиться пользователям паттерна BLoC, которым ранее пришлось объединять несколько типов данных в одном снимке (`snapshot`) или использовать несколько `StreamBuilder` для каждого типа данных.
Кроме того, можно использовать `LayoutBuilder` вместе с `Provider.of<T>(context)`:
```
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
var counter = Provider.of<ProviderModel>(context);
return Text('Provider ${counter.count}');
},
)
```
Кроме того, существуют различные виды `Provider`, такие как `ValueListenableProvider`, `FutureProvider` и `StreamProvider`. Это свидетельствует о том, что дизайн всего **Provider** ближе к нативным особенностям Flutter, при этом он проще в понимании и также учитывает вопросы производительности.Подробное руководство по использованию **Provider** уже было написано Vadaski в его статье ["Flutter | Гайд по управлению состоянием — Provider"](https://juejin.im/post/5d00a84fe51d455a2f22023f); поэтому я не буду повторяться, заинтересованным стоит обратиться к нему.
> Таким образом, пятнадцатая часть наконец завершена! (///▽///)
### Рекомендации по материалам
* Пример кода для этой статьи: <https://github.com/CarGuo/state_manager_demo>
* GitHub: <https://github.com/CarGuo/>
* **Открытый проект Flutter:** <https://github.com/CarGuo/GSYGithubAppFlutter>
* **Множество примеров использования Flutter:** <https://github.com/CarGuo/GSYFlutterDemo>
* **Открытая книга по Flutter:** <https://github.com/CarGuo/GSYFlutterBook>
#### Рекомендация полных открытых проектов:
* [GSY Flutter учебник](https://github.com/CarGuo/GSYFlutterBook)
* [GSYGithubApp Flutter](https://github.com/CarGuo/GSYGithubAppFlutter)
* [GSYGithubApp React Native](https://github.com/CarGuo/GSYGithubApp)
* [GSYGithubAppWeex](https://github.com/CarGuo/GSYGithubAppWeex)

Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )