Оригинальное название – Demystifying State Management in Flutter: An In-Depth Exploration Управление состоянием – это фундаментальная концепция всех фреймворков для разработки приложений, и Flutter не является исключением. Управление состоянием – это то, как приложение может управлять данными, которые оно использует, и обновлять их. Во Flutter существует несколько техник и инструментов, которые можно использовать для управления состоянием, и выбор наиболее подходящего из них часто зависит от сложности и требований приложения, которое вы создаете. В этой статье я хочу рассмотреть несколько отличных способов управления состоянием вашего приложения в зависимости от конкретных потребностей. Предположим, вы разрабатываете приложение для электронной коммерции, в котором пользователь может просматривать различные товары, добавлять их в корзину и, наконец, оформлять заказ. Данные корзины (добавленные товары, их количество, цены и т.д.) очень важны, и изменения, внесенные в них, должны отражаться на всём приложении. Например: Экран списка продуктов: если пользователь добавляет товар в корзину, интерфейс должен отражать это (например, показывать на товаре значок "В корзине"). Экран корзины: на этом экране всегда должна отображаться актуальная информация обо всех товарах, находящихся в корзине, их количестве и общей цене. Экран сведений о продукте: если товар уже находится в корзине, это должно быть отражено на экране сведений о продукте (например, путем замены кнопки "Добавить в корзину" на кнопку "Удалить из корзины"). Учитывая эти требования, становится ясно, что управление состоянием корзины очень важно для обеспечения стабильного пользовательского опыта. Без надлежащего управления состоянием отслеживание данных корзины на разных экранах и обеспечение соответствия пользовательского интерфейса текущему состоянию превратилось бы в сложную и чреватую ошибками задачу. Давайте на примере этой концепции корзины посмотрим, как можно использовать ее для демонстрации управления состоянием. Provider – это инструмент управления состоянием во Flutter, который позволяет виджетам получать доступ к общим данным и перестраиваться при их изменении. Он использует концепцию инъекции зависимостей, предоставляя данные там, где они необходимы в дереве виджетов, и перестраивая только те виджеты, которым важна эта часть данных. Давайте используем его в нашем приложении корзины. Во-первых, необходимо создать модель, расширяющую ChangeNotifier. Эта модель будет представлять нашу корзину: Теперь мы предоставим эту модель нашим виджетам с помощью ChangeNotifierProvider: В Flutter ChangeNotifier – это класс, входящий в состав Flutter SDK, который предоставляет слушателям уведомления об изменениях. Далее, в любом месте дерева виджетов мы можем получить доступ к нашей модели CartModel и перестроить ее при изменении: В этом упрощенном примере мы имеем корзину, в которой хранится только количество товаров. Модель CartModel предоставляется дереву виджетов с помощью ChangeNotifierProvider, и все виджеты, зависящие от корзины (например, MyCartIcon), могут прослушивать корзину и перестраиваться при ее изменении. Виджеты, которые изменяют корзину, но не нуждаются в ее прослушивании (например, MyAddButton), могут обращаться к ней с дополнительным параметром listen: false при вызове of. Прим. пер.: давным-давно, начиная с версии 4.1.0, были добавлены прекрасные расширения для BuildContext – context.watch<T> и context.read<T>(). Они изысканней и короче, и синтаксически яснее выражают намерения разработчика (возможно, только для тех, у кого Riverpod прогерского мозга). По своей сути Riverpod – это библиотека управления состоянием для Flutter, которая призвана улучшить Provider. Riverpod обеспечивает более безопасный, тестируемый и гибкий способ управления состоянием и его совместного использования в приложении. Давайте интегрируем Riverpod в наше приложение. Сначала определим провайдера. Вы создаете провайдера, который будет хранить и экспонировать ваше состояние. Это можно сделать различными способами в зависимости от ваших потребностей, но для простоты давайте создадим StateProvider: В этом примере cartProvider – это провайдер, который содержит целочисленное состояние, представляющее количество товаров в корзине. Начальное состояние устанавливается равным 0. Теперь давайте получим доступ и изменим состояние. В Riverpod виджеты могут получать доступ к состоянию и изменять его, используя ref.watch() для наблюдения за провайдером и ref.read() для чтения провайдера: Далее необходимо указать провайдера. В отличие от Provider, в Riverpod вам не нужно оборачивать свое приложение провайдерами. Для этого достаточно обернуть приложение в ProviderContainer: В этом простом примере cartProvider хранит состояние корзины, а виджеты MyCartIcon и MyAddButton могут как читать, так и изменять это состояние. При изменении состояния cart.state все виджеты, наблюдающие за cartProvider, перестраиваются. Прим. пер.: На данный момент существует три способа получения состояния провайдера – ref.watch для прослушивания состояния и перестройки виджета (запрещается использовать в обратных вызовах и методах, подобных initState() ; можно использовать в методе Widget build(...) и в других провайдерах), ref.read для одноразового чтения значения (не рекомендуется использовать в методе сборки и внутри других провайдеров, т.к. при перестройке/обновлении провайдера, читающие его будут использовать старый экземпляр, что чревато ошибками) и ref.listen – можно использовать где угодно: слушайте новые значения и делайте какие-либо действия на основе этого (удобно показывать toast|snack, использовать декларативную навигацию или логгировать какие-либо действия). Автор не указал (код верный), что теперь мы должны использовать ConsumerWidget и ConsumerStatefulWidget вместо StatelessWidget и StatefulWidget соответственно, которые содержат в себе некоторую логику по работе с riverpod + добавляют аргумент WidgetRef ref в метод build() (или поле в случае c ConsumerStatefulWidget). На самом деле, если заглянуть под капот, то оба из них наследуются от одного и того же класса – StatefulWidget. Также, как и в Provider, в Riverpod есть виджет Consumer, который можно использовать в нужном месте внутри метода build, чтобы не перестраивать лишний раз дерево тех виджетов, что не нуждаются в значениях. Bloc (Business Logic Component) и Cubit – решения для управления состоянием, входящие в состав пакета flutter_bloc. Bloc работает на основе событий и состояний, в то время как Cubit, более простая версия Bloc, работает непосредственно с изменениями состояния. Оба они предназначены для управления состоянием путем отделения бизнес-логики от пользовательского интерфейса. Снова интегрируем пакет в наше приложение корзины. Во-первых, необходимо дать определение кубита. Вы создаёте класс Cubit, который будет хранить и управлять вашим состоянием: В данном примере CartCubit – это Cubit, который хранит целочисленное состояние, представляющее количество товаров в корзине. Начальное состояние устанавливается равным 0. Теперь мы можем использовать Cubit. Виджеты могут обращаться к Cubit и реагировать на изменения состояния: И, наконец, нам необходимо предоставить Cubit. Чтобы сделать Cubit доступным для виджетов, обычно используется BlocProvider: В этом простом примере CartCubit хранит и управляет состоянием корзины, а виджеты MyCartIcon и MyAddButton могут прослушивать и изменять это состояние. При изменении состояния cartCubit.state все виджеты, слушающие CartCubit, перестраиваются. Redux – это библиотека управления предсказуемыми состояниями, реализующая идею однонаправленного потока данных. Он управляет состоянием приложения в едином неизменяемом дереве состояний, которое обновляется путем диспетчеризации действий редукторам, создающим новое состояние. Давайте заиспользуем его в нашем приложении. Сначала определим State и Reducer. Вы определяете модель состояния и функцию, называемую редуктором, которая определяет, как изменяется состояние в ответ на определенные действия: Давайте создадим Store. Store объединяет состояние и редуктор. Он создается в корне вашего приложения и передается всем дочерним элементам: Далее воспользуемся Store. Виджеты могут обращаться к хранилищу для получения состояния и диспетчеризации действий: В данном примере у нас есть AppState, представляющий состояние нашего приложения, и cartReducer, определяющий, как это состояние изменяется в зависимости от действий. При диспетчеризации (отправке) действия (например, CartActions.AddItem) редуктор генерирует новое состояние, что приводит к перестройке прослушивающих виджетов. MobX – это библиотека управления состоянием, в которой применяется концепция реактивного программирования. Он разделяет состояние и пользовательский интерфейс приложения и автоматически обновляет интерфейс при изменении состояния. Давайте сделаем тот же функционал в последний раз в нашем приложении корзины.   Во-первых, определим Store – класс, в котором хранится ваше состояние и действие, изменяющее это состояние: В данном примере CartStore – это хранилище MobX, которое содержит наблюдаемое состояние itemCount, представляющее количество товаров в корзине, и действие incrementItemCount для изменения этого состояния. Теперь мы понаблюдаем за состоянием. Виджеты могут наблюдать за состоянием и автоматически перестраиваться при его изменении: В этом простом примере CartStore хранит и управляет состоянием корзины, а виджеты MyCartIcon и MyAddButton могут прослушивать и изменять это состояние. При изменении cartStore.itemCount все виджеты, наблюдающие за состоянием, перестраиваются. Теперь, когда мы рассмотрели множество способов управления состоянием Flutter, какой из них использовать? Выбор решения для управления состоянием часто зависит от конкретного случая использования, сложности приложения, знакомства команды с паттерном и личных предпочтений. Вот общие рекомендации по использованию каждого из этих решений: Provider: это хороший выбор для приложений любого размера, особенно если вы стремитесь к простоте и отсутствию шаблонов. Он обеспечивает хороший баланс между сложностью и функциональностью, что делает его надежным выбором для многих приложений. Например, если вы создаете персональный проект или приложение среднего размера и хотите получить что-то простое для понимания и реализации, вам подойдет Provider. Riverpod: Это шаг вперёд по сравнению с Provider. Он сохраняет простоту Provider, но стремится преодолеть некоторые его недостатки, такие как сложность работы с несколькими провайдерами и неловкость при чтении провайдера извне дерева виджетов. Riverpod хорошо подходит, когда вы начинаете чувствовать себя ограниченным возможностями Provider. Например, если вы работаете над более крупным проектом с более сложными потребностями в управлении состоянием или вам требуется бо́льший контроль над жизненным циклом ваших провайдеров. Bloc/Cubit: Эти варианты хороши для крупных приложений или приложений, требующих сложного управления состоянием. Они обеспечивают четкое разделение между бизнес-логикой и представлением информации, что делает их хорошим выбором для крупных проектов или проектов, в которых бизнес-логика может быть сложной или подверженной изменениям. Например, для приложений электронной коммерции со сложными бизнес-правилами, такими как система скидок, аутентификация пользователей и отслеживание заказов, может подойти Bloc/Cubit. Redux: Redux отлично подходит для больших приложений, где требуется контейнер с предсказуемым состоянием, обеспечивающий однонаправленный поток данных. Это также хороший выбор, если вы хотите получить надежный опыт отладки, поскольку Redux DevTools может обеспечить отладку с перемещением во времени. Если вы работаете над масштабным проектом с командой, знакомой с Redux, и цените предсказуемость и строгую структуру, которую навязывает Redux, то этот вариант вам подойдет. MobX: MobX подходит для тех, кто предпочитает более реактивный стиль программирования. Это хороший выбор, если вам нужен тонкий контроль над реакциями на изменение состояния и не мешает магия под капотом. Например, если у вас есть приложение, в котором вы хотите, чтобы определенные части пользовательского интерфейса реагировали на определенные состояния, или если вы являетесь специалистом в области JavaScript и имеете опыт работы с MobX, это может быть правильным выбором. Помните, что оптимальное решение зависит от специфики проекта, опыта команды и ваших личных предпочтений. Часто бывает полезно поэкспериментировать с несколькими вариантами, чтобы понять, какой из них вам больше нравится. Спасибо, что читаете! Я надеюсь, что эта статья была для вас полезной и приятной. Если вы это сделали, пожалуйста, следите за мной, чтобы получать больше материалов, подобных этому. Ваша поддержка очень много значит для меня и мотивирует меня продолжать писать. Если у вас есть вопросы или комментарии к этому посту, пожалуйста, оставьте их ниже. Я буду рад ответить на любые Ваши вопросы. Кроме того, вы можете связаться со мной в Twitter (jordan_goulet) или (UbermenschDev), чтобы узнать больше о моей работе. Еще раз спасибо, что читаете, и удачного дня! Материал переведён Ruble.  Моя литера Т стремится к равнозначности линий