1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-15.md 25 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Этот пост поможет вам глубже понять механизм работы состояний в Flutter и через анализ библиотеки управления состояниями Provider сделать это еще более понятным. После прочтения этой статьи вы сможете легче осмыслить ваш "гнездо состояний".

Содержание статьи:

Полный курс по Flutter с примерами

Серия статей о мире Flutter

⚠️ В двенадцатой статье больше внимания уделяется анализу библиотек управления состоянием, а эта статья больше сосредоточена на внутренней реализации состояний в Flutter.

1. Состояние

1. Что такое состояние?

Мы знаем, что во вселенной Flutter все объекты являются Widget, а Widget является @immutable, то есть неизменяемым, поэтому каждый Widget представляет собой отдельную кадровую картинку.

На этом основании, StatefulWidget использует State для реализации перерисовки между кадрами, то есть при каждом перерисовывании Widget, State предоставляет новые данные для перерисовки.

2. Как состояние обеспечивает перерисовку между кадрами?

Для этого важно понять принцип реализации 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, что может привести к часто игнорируемому новичками вопросу.

Давайте сначала рассмотрим проблемный код, как показано на следующем рисунке:

    1. В _DemoAppState мы создали DemoPage, передав ему переменную data.
    1. При создании состояния createState в DemoPage переменная data была передана непосредственно в _DemoPageState.
    1. В _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 будет автоматически обновлён.

3. Что делает setState?

Часто упоминаемый setState фактически вызывает метод markNeedsBuild. Метод markNeedsBuild помечает element как dirty, а затем он будет перерисован в следующей кадровой рамке (WidgetsBinding.drawFrame). Это также указывает на то, что setState не действует сразу.

4. Поделиться состоянием

Ранее мы говорили о роли и принципах работы объекта 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`.

![Схема](http://img.cdn.guoshuyu.cn/2 Yöntem-Flutter-15/image5)

> Дальше начинается реальная аналитика **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`, что приводит к обновлению.

![image](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image6)

Используемый нами `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)

![ ](http://img.cdn.guoshuyu.cn/20190616_Flutter-15/image7)

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master