С развитием Flutter за последние годы появились множество фреймворков управления состоянием, а в течение последнего года наиболее рекомендованным официальной командой фреймворком управления состоянием стал Riverpod
. Даже превзошел Provider
, и сам Riverpod
называет себя "обновленной версией Provider
".
Сам
Provider
описывается как "упакованная версияInheritedWidget
, но более простая и повторно используемая". АRiverpod
представляет собой рефакторингProvider
с новыми возможностями.
Для сравнения фреймворков управления состоянием за прошедший год можно обратиться к статье "Flutter состояние управления 2021: Как выбрать?". Основная цель данной статьи — раскрыть внутреннюю реализацию Riverpod
, понять его принцип работы и то, почему он требует меньше шаблонов и не зависит от BuildContext
.
Если спросить, какой самый заметный признак Riverpod
, ответ будет очевиден — это отсутствие зависимости от BuildContext
(то есть использование другого типа зависимости), что позволяет ему легко достигать следующего эффекта:
То есть Provider
в Riverpod
может быть глобальным и не требовать зависимости от BuildContext
для реализации бизнес-логики.
⚠️ Предупреждение: здесь и далее упоминание
Provider
и библиотекиprovider
не связано.Тогда как же реализуетсяRiverpod
внутри? Давайте начнем исследование принципов работыRiverpod
.
Реализация
Riverpod
достаточно сложна, поэтому будьте терпеливы и продолжайте читать, так как эта статья пошагово объясняет процесс, поэтому если вы столкнетесь с некоторыми трудностями, не беспокойтесь, после прочтения всей статьи вам станет всё яснее.
В 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
. Обычно при использовании 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
как входную точку для нашего анализа.
Когда создаются "провайдеры" в 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.
Шаг за шагом это выглядит так:
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
.
Поэтому внутри ProviderContainer
происходит следующее:- 1) Сначала создается _StateReader
на основе переданного provider
при вызове read
;
_StateReader
сохраняется в карте _stateReaders
с provider
в качестве ключа, а затем возвращается;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
中.
Обычное определение метода 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
:
В 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
.
⚠️ Обратите внимание, что здесь есть место для ошибки восприятия — это либо
ProviderFamily
, либоFamilyProvider
. Мы получаемFamilyProvider
изProviderFamily
и передаем его какProviderBase
вref.watch
.
Долго не писал такого подробного анализа исходного кода, а вечером уже была полночь. Хотя Riverpod гораздо сложнее, поэтому чтение его требует больше времени, но использование становится более удобным, особенно когда нет ограничений от BuildContext
, хотя это также приводит к зависимости от ConsumerWidget
. Все за и против зависят только от ваших потребностей, но в целом Riverpod — это отличная библиотека, которую стоит попробовать.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )