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

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

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

С развитием Flutter за последние годы появились множество фреймворков управления состоянием, а в течение последнего года наиболее рекомендованным официальной командой фреймворком управления состоянием стал Riverpod. Даже превзошел Provider, и сам Riverpod называет себя "обновленной версией Provider".

Сам Provider описывается как "упакованная версия InheritedWidget, но более простая и повторно используемая". А Riverpod представляет собой рефакторинг Provider с новыми возможностями.

Для сравнения фреймворков управления состоянием за прошедший год можно обратиться к статье "Flutter состояние управления 2021: Как выбрать?". Основная цель данной статьи — раскрыть внутреннюю реализацию Riverpod, понять его принцип работы и то, почему он требует меньше шаблонов и не зависит от BuildContext.

Введение

Если спросить, какой самый заметный признак Riverpod, ответ будет очевиден — это отсутствие зависимости от BuildContext (то есть использование другого типа зависимости), что позволяет ему легко достигать следующего эффекта:

То есть Provider в Riverpod может быть глобальным и не требовать зависимости от BuildContext для реализации бизнес-логики.

⚠️ Предупреждение: здесь и далее упоминание Provider и библиотеки provider не связано.Тогда как же реализуется Riverpod внутри? Давайте начнем исследование принципов работы Riverpod.

Реализация Riverpod достаточно сложна, поэтому будьте терпеливы и продолжайте читать, так как эта статья пошагово объясняет процесс, поэтому если вы столкнетесь с некоторыми трудностями, не беспокойтесь, после прочтения всей статьи вам станет всё яснее.

Начнем с ProviderScope

В Flutter любое использование управления состоянием неизбежно приведёт к использованию InheritedWidget, и в случае Riverpod тоже. В Riverpod всегда используется ProviderScope, который обычно регистрируется на уровне всего приложения.

Если у вас есть вопросы относительно InheritedWidget, вы можете найти полезную информацию в моей статье на Juejin: «Полное понимание State и Provider».

Начнём с примера, представленного ниже, это простой пример из официальной документации:

Пример

  • Вложите верхний уровень ProviderScope;
  • Создайте глобальный StateProvider;
  • Используйте ref в ConsumerWidget, чтобы прочитать созданный counterProvider с помощью метода read, увеличив значение типа int;
  • Используйте ref в другом Consumer, чтобы отслеживать изменения в созданном counterProvider с помощью метода watch, получая каждое изменение значения типа int.

Очень простой пример, можно заметить, что нет ни одного использования of(context). Глобальные данные в counterProvider можно читать и отслеживать через ref, а также правильно считывать и обновлять их.

Но как это реализовано? Как counterProvider был внедрен в ProviderScope? Почему мы не видим context? Ответив на эти вопросы, продолжим изучение.

Сначала рассмотрим ProviderScope, который является единственным верхним уровнем InheritedWidget. Поэтому counterProvider обязательно хранится здесь:

В RiverPod ProviderScope служит для предоставления ProviderContainer.

Более конкретно, он предоставляется через внутренне вложенный UncontrolledProviderScope. Таким образом, мы можем заключить, что ProviderScope может обеспечивать распространение состояния вниз, так как внутри него есть InheritedWidget, а основной механизм распространения — это класс ProviderContainer.

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

На самом деле официальное определение ProviderContainer звучит следующим образом: «Контейнер для хранения состояний различных Providers и поддержка переопределения поведения некоторых специальных `Providers».

ProviderContainerЗдесь появился новый класс, называемый ProviderContainer. Обычно при использовании RiverPod вам не нужно знать о нем, так как вы обычно не работаете напрямую с этим контейнером, но каждый ваш шаг при работе с RiverPod связан с его реализацией, например:- ref.read требует его метода Result read<Result>;

  • ref.watch требует его метода ProviderSubscription<State> listen<State>;
  • ref.refresh требует его метода Created refresh<Created>;

Даже сохранение и чтение различных Providers часто связаны с этим контейнером. Таким образом, этот класс выполняет ключевые операции по управлению различными Providers в RiverPod.

"Провайдер" и "Элемент"

Как мы уже знали, ProviderScope передает вниз ProviderContainer. Но как же работает сам Provider, почему ref.watch / ref.read могут получить доступ к его значениям?

Продолжая с примерами выше, здесь мы просто определяем StateProvider и используем ref.watch, но как это позволяет нам получать доступ к значению state внутри него?

Сначала отметим, что StateProvider является специальным типом Provider, внутри которого есть ещё один объект _NotifierProvider, который выполняет преобразование. Поэтому мы начнём анализировать самый базовый класс Provider.

Базовые Provider обычно являются подклассами ProviderBase, поэтому мы будем анализировать именно этот класс.

Внутри RiverPod каждый подкласс ProviderBase имеет свой соответствующий подкласс ProviderElementBase, например, StateProvider является подклассом ProviderBase, а также он имеет соответствующий подкласс StateProviderElement, являющийся подклассом ProviderElementBase.

Итак, в RiverPod каждому "провайдеру" соответствует свой "элемент".

⚠️ В данном контексте "элемент" не относится к понятию "элемент" в Flutter, где это часть трёх деревьев. Это уникальная единица в RiverPod, которая является подклассом объекта Ref. Объект Ref предоставляет интерфейсы взаимодействия между "провайдерами" в RiverPod и абстрактные методы жизненного цикла.

А какие функции выполняют "провайдеры" и "элементы"?

Во-первых, при создании StateProvider мы передаем (ref) => 0, что фактически представляет собой функцию Create<State, StateProviderRef<State>>. Мы будем использовать эту функцию Create как входную точку для нашего анализа.

Create<T, R extends Ref> = T Function(R ref)

Когда создаются "провайдеры" в RiverPod, они принимают функцию Create, внутри которой мы можем реализовать необходимую бизнес-логику, например, counterProvider использует () => 0, чтобы вернуть значение типа int равное нулю при инициализации. Особенно важно то, что эта функция определяет тип State.

Если добавить <int> к коду выше, становится ещё более очевидным, что на самом деле State является шаблонным параметром, и когда мы определяем "провайдер", мы должны указывать тип этого шаблонного параметра State, например, здесь это int.

Возвращаемся к обычному вызову Provider, переданный нами Create метод выполняется внутри ProviderElementBase.

Как показано на приведённой выше схеме, можно сказать, что когда ProviderElementBase вызывает "setState", он запускает выполнение create метода, который затем получает определённый нами тип State и возвращает его как result, после чего происходит уведомление и обновление UI.

⚠️ Здесь "setState" также не является setState из Flutter Framework, а представляет собой собственный "setState" метод внутри RiverPod, не связанный с State из Flutter фреймворка.

Поэтому каждый "provider" имеет свой собственный "element". При создании "provider" переданный нами create метод вызывается внутри "element" через setState.

setState внутри "element" главным образом использует новый newState для получения объекта result из RiverPod, а затем через _notifyListeners обновляет этот result в местах вызова watch.

Основная роль result заключается в предоставлении данных через result.data, result.error, map и requireState. Обычно состояние получается через requireState, что отражено в RiverPod следующим образом:

Когда мы вызываем read(), это в конечном итоге вызывает element.readSelf();, то есть возвращает requireState (что обычно и является нашим определённым типом State).

Не кажется ли вам всё это сложным?Проще говоря, после создания "Provider" внутри "Element" вызывается setState(_provider.create(this)), запуская переданный нами create метод и передавая сам "Element" как ref, поэтому ref, который мы используем, фактически является ProviderElementBase.> Поэтому названия в RiverPod имеют логическое объяснение, поскольку отношение между "Provider" и "Element" напоминает отношения между Widget и Element в Flutter.

Шаг за шагом это выглядит так:

  • При создании "Provider" мы передаём один create метод;
  • Этот create метод вызывается внутренним setState в ProviderElementBase, чтобы получить result;
  • Внутри result метод requireState позволяет нам при использовании read() получать значение нашего определённого типа State. ## WidgetRef

Ранее было рассмотрено множество аспектов, но до сих пор не объяснено, как StateProvider связан с ProviderScope, то есть, как "Provider" связан с ProviderContainer. На чем основано использование ref.read для чтения состояния (State) ?

В наших примерах использовались компоненты ConsumerWidget и Consumer, которые являются одним и тем же объектом. А сам ref представляет собой тот самый "Элемент", который мы упоминали ранее, или, другими словами, ProviderElementBase.

Из исходного кода видно, что логика ConsumerWidget реализуется в ConsumerStatefulElement, который наследует StatefulElement и реализует интерфейс WidgetRef.

По этому коду можно заметить знакомые элементы: ProviderScope, ProviderContainer, WidgetRef.Сначала рассмотрим метод ProviderScope.containerOf(this), где наконец встречаем знакомый нам BuildContext. Этот метод фактически является аналогом часто используемого of(context), но он используется внутри ConsumerStatefulElement для получения ProviderContainer, переданного через ProviderScope.

Таким образом, ConsumerStatefulElement получает доступ к ProviderContainer, поэтому ConsumerStatefulElement может использовать методы read и watch от ProviderContainer.

Затем обратимся к тому, что ConsumerStatefulElement реализует интерфейс WidgetRef, следовательно, используемый нами WidgetRef — это сам ConsumerStatefulElement.

То есть вызов ref.read эквивалентен вызову read у ConsumerStatefulElement, который затем выполняется для ProviderContainer.

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

Здесь важно не смешивать Element из Flutter с ProviderElementBase из RiverPod. Таким образом, интерфейс WidgetRef стал абстракцией для Element, заменив BuildContext. Это одно из "волшебств" Riverpod.

Чтение данных (read)

После того как мы прояснили отношения и функции между ProviderScope, Provider, ProviderElementBase, ProviderContainer, ConsumerWidget (или ConsumerStatefulElement) и WidgetRef, можно приступить к анализу цепочки действий метода read.

Поняв концепцию и роль этих компонентов, можно провести анализ процесса с использованием ref.read. В целом это выглядит так:- ConsumerWidget получает доступ к общему ProviderContainer на верхнем уровне через внутренний ConsumerStatefulElement;

  • Когда мы вызываем read/watch через ref, фактически используем ConsumerStatefulElement для обращения к методу read внутри ProviderContainer;Схема работы

Окончательной задачей является понимание того, как метод read внутри ProviderContainer получает доступ к состоянию (State).

Для этого следует обратиться к ранее рассмотренному ProviderElementBase. На самом деле, при выполнении метода read в ProviderContainer используется метод readProviderElement.

Метод readProviderElement предназначен для получения соответствующего Element через Provider. Например:

ref.read(counterProvider)

Общими словами, read/watch — это получение ProviderElementBase ("Element") из ProviderContainer с помощью provider в качестве ключа. В этом процессе также используется новый объект, называемый: _StateReader.

Ключевой момент метода readProviderElement заключается в получении _StateReader. Внутри ProviderContainer есть внутренняя переменная _stateReaders, которая представляет собой карту для кэширования _StateReader.

Структура _stateReaders

Пример использования _StateReader

Поэтому внутри ProviderContainer происходит следующее:- 1) Сначала создается _StateReader на основе переданного provider при вызове read;

    1. _StateReader сохраняется в карте _stateReaders с provider в качестве ключа, а затем возвращается;
    1. Через метод getElement() объекта _StateReader получается или создаётся ProviderElementBase. > Здесь ключом в _stateReaders является ProviderBase, а значением — _StateReader. Это просто хранение "поставщика" в ProviderContainer, то есть связывание его с ProviderScope. Таким образом, "поставщик" и ProviderScope теперь связаны вместе. Не использовал явного BuildContext и лишних вложений, чтобы связать Provider с ProviderScope. Кроме того, здесь можно видеть, как через ref.read получается ProviderElementBase, используя provider.

Получив ProviderElementBase, помните раздел, где мы рассказывали о "Provider" и "Element"? ProviderElementBase вызывает метод setState, чтобы выполнить переданную Create-функцию и вернуть Result в виде State.

Здесь видно, что после получения ProviderElementBase выполняется return element.readSelf(), что фактически эквивалентно вызову requireState.

С момента самого простого процесса ref.read в RiverPod вся логика стала доступной:

  • ProviderScope распространяет ProviderContainer;
  • Внутри ConsumerWidget ConsumerStatefulElement через BuildContext получает доступ к ProviderContainer и реализует интерфейс WidgetRef;
  • Через метод read(provider) интерфейса WidgetRef происходит обращение к методу read внутри ProviderContainer;
  • ProviderContainer использует метод read для создания или получения ProviderElementBase через provider;
  • ProviderElementBase выполняет create-функцию внутри provider, чтобы получить result в виде state.

Другие процессы watch и refresh имеют аналогичную структуру, но отличаются более сложной внутренней логикой, особенно при обновлении:

Через метод ref.refresh запускается метод refresh внутри ProviderContainer, который в конечном итоге вызывает выполнение setState(_provider.create(this)) через _buildState.Анализируя этот процесс, становится понятно, как RiverPod обеспечивает связь без использования явного BuildContext.

Дополнительный анализ

Основные этапы вызова были рассмотрены выше. Здесь будут представлены некоторые дополнительные аспекты, такие как использование различных типов "Element", таких как ProviderElement, StreamProviderElement, FutureProviderElement и других подклассов ProviderElementBase. Мы выяснили, что они не являются элементами Element в Flutter, а представляют собой единицы состояния в Riverpod для управления состоянием Provider. Например, FutureProviderElement предоставляет AsyncValue<State> на основе ProviderElementBase,主要用于 FutureProvider.

AsyncValue

Обычное определение метода create в RiverPod выглядит следующим образом:

Однако в случае FutureProvider добавляется еще одна функция _listenFuture. После выполнения этой функции значение будет иметь тип AsyncValue<State>.

При выполнении _listenFuture, сначала вызывается AsyncValue<State>.loading(), а затем, в зависимости от результата Future, возвращается либо AsyncValue<State>.data, либо AsyncValue<State>.error.

Поэтому при использовании read / watch, возвращаемый тип requireState становится AsyncValue<State>.

Для работы с AsyncValue были созданы расширения (extensions). В этих расширениях есть методы для получения значений типа AsyncData, такие как data и asData, а также методы для создания различных состояний, например, метод when:

autoDispose и family

В RiverPod часто используется статическая переменная autoDispose и family, которая присутствует практически во всех провайдерах. Для чего они нужны?

Например, в нашем примере мы используем FutureProvider со свойством autoDispose:

На самом деле FutureProvider.autoDispose представляет собой AutoDisposeFutureProvider, аналогично это верно для других провайдеров.

Если обычный Provider наследуется от AlwaysAliveProviderBase, то AutoDisposeProvider наследуется от AutoDisposeProviderBase:

Из названий можно понять:

  • AlwaysAliveProviderBase всегда активен;
  • AutoDisposeProviderBase автоматически уничтожается, когда его больше никто не слушает;

То есть, когда внутренние списки _listeners, _subscribers и _dependents пусты, и состояние maintainState равно false, происходит уничтожение провайдера.> Проще говоря, это "все или ничего" подход.

Например, когда мы вызывали read, всегда использовался метод mayNeedDispose для попытки освобождения:

Освобождение включает вызов element.dispose() и удаление из карты _stateReaders.

Аналогично, family соответствует ProviderFamily, который используется для:> построения провайдера с помощью дополнительных аргументов, то есть добавление одного параметра.

Например, по умолчанию имеется:

final tagThemeProvider = Provider<TagTheme>;

что можно преобразовать в:

final tagThemeProvider2 = Provider.family<TagTheme, Color>;

Затем вы можете использовать дополнительные параметры при вызовах read/watch:

final questionsCountProvider = Provider.autoDispose((ref) {
  return ref
    .watch(tagThemeProvider2(Colors.red));
});

Этот функционал реализован через ProviderFamily. В отличие от обычного Provider, у которого есть метод create, у ProviderFamily он такой:

Можно заметить, что create создает новый Provider, то есть внутри family находится вложенный Provider.

Поэтому, используя пример выше, раньше мы могли просто использовать ref.watch(tagThemeProvider);, так как наш tagThemeProvider был прямым экземпляром ProviderBase.

Однако использование ref.watch(tagThemeProvider2); приведет к ошибке:

Тип аргумента 'ProviderFamily<TagTheme, Color>' нельзя назначить параметру типа 'ProviderListenable<dynamic>'.

Дело в том, что здесь происходит вложение Provider в Provider, поэтому сначала мы получаем ProviderFamily<TagTheme, Color>, и нам нужно заменить его на ref.watch(tagThemeProvider2(Colors.red));.

Вызов tagThemeProvider2(Colors.red) приводит к тому, что мы получаем нужный ProviderBase.

Тогда почему ProviderFamily выполняется таким образом? Ведь у него нет такого конструктора.

Это связано с особенностями языка Dart, если вас интересует подробнее, см.: https://juejin.cn/post/6968369768596242469

Первоначально мы получаем ProviderFamily<TagTheme, Color>. В Dart все функции являются подтипами Function, поэтому они имеют встроенный метод call. Мы выполняем tagThemeProvider2(Colors.red), что фактически вызывает метод call в ProviderFamily, который затем вызывает метод create, чтобы получить FamilyProvider<State>. FamilyProvider является подклассом ProviderBase.

image

⚠️ Обратите внимание, что здесь есть место для ошибки восприятия — это либо ProviderFamily, либо FamilyProvider. Мы получаем FamilyProvider из ProviderFamily и передаем его как ProviderBase в ref.watch.

Последнее

Долго не писал такого подробного анализа исходного кода, а вечером уже была полночь. Хотя Riverpod гораздо сложнее, поэтому чтение его требует больше времени, но использование становится более удобным, особенно когда нет ограничений от BuildContext, хотя это также приводит к зависимости от ConsumerWidget. Все за и против зависят только от ваших потребностей, но в целом Riverpod — это отличная библиотека, которую стоит попробовать.

Опубликовать ( 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