Салют! Меня зовут Ваня Берсенев и в этой статье я постараюсь спасти от выгорания твой джуновский энтузиазм, впервые столкнувшийся с одним из главных боссов Flutter’а — стейт менеджментом.
Хочешь больше узнать про флаттер, архитектуру и алгоритмы для собеседований? Подписывайся на мой канал t.me/vanyakodit
Стейт-менеджмент — одна из самых неоднозначных тем, с которой сталкиваются все новички, начинающие изучать Flutter. В этой статье ты поймешь суть стейт-менеджмента, напишешь свой примитивный стейт-менеджер, а затем освоишь основы управления состояниями с помощью Bloc.
Для начала разберёмся с тем, что такое стейт-менеджмент.
Для начала разберёмся с тем, что такое стейт-менеджмент
Занимаясь мобильной разработкой, необходимо вбить в себе в голову тот факт, что всё, что мы видим на экране — это набор состояний.
-
Сраница, которую видит пользователь — это состояние.
-
Цвет страницы — это состояние.
-
Размер шрифта — это состояние.
-
Циферка, показывающая количество денег на счёте — тоже состояние.
Всё во флаттере — виджеты, а каждый виджет, в свою очередь, может иметь безграничное количество состояний, а может и не иметь их вовсе. То есть любой виджет — это просто конфигуратор, в который мы кладём описание состояния, которое хотим показать на экране.
И наша цель — научиться эффективно управлять тем, какое состояние и в какой момент видит пользователь.
Пример
Создадим на экране обычный Container()
. Даже не будем ничего туда класть
Код
void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Container(), ), ), ); } }
Посмотрим, что на экране:
Что же мы увидели на экране? Правильно, ничего. Почему? Потому что мы добавили виджет, но не добавили к нему ни одного состояния. Добавим нашему контейнеру его первое состояние путём добавления новых полей. Вместо пустого Container()
покажем
Container(
color: Colors.green,
width: 100,
height: 100,
)
Код
void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Container( color: Colors.green, width: 100, height: 100, ), ), ), ); } }
Посмотрим, что теперь на экране:
Вау! Зелёный квадрат! И это, как ты уже понял, наше первое состояние.
А теперь предположим, что нам необходимо показывать пользователю различные виджеты в зависимости от того авторизован он или нет.
Например, если пользователь авторизован, мы будем показывать ему зеленый квадрат, а если не авторизован — красный.
Получается, что нам нужно:
-
Добавить ещё одно состояние
-
Показывать нужный виджет в зависимости от того, какой стейт сейчас активный.
Код будет ниже. Сначала посмотри, что получилось, а потом подумай как это реализовано.
Теперь посмотри на код и попробуй самостоятельно понять, как это работает
Код
sealed class ContainerState {} class AuthorizedState extends ContainerState {} class NotAuthorizedState extends ContainerState {} class App extends StatefulWidget { const App({super.key}); @override State<App> createState() => _AppState(); } class _AppState extends State<App> { ContainerState state = NotAuthorizedState(); // Метод изменяет текущее состояние на противоположное void changeState() { setState(() { if (state is AuthorizedState) { state = NotAuthorizedState(); } else { state = AuthorizedState(); } }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( floatingActionButton: FloatingActionButton( child: state is AuthorizedState ? const Text('log out') : const Text('log in'), onPressed: () { changeState(); }, ), body: Center( child: SizedBox( height: 100, width: 100, child: switch (state) { AuthorizedState() => const ColoredBox(color: Colors.green), NotAuthorizedState() => const ColoredBox(color: Colors.red), }, ), ), ), ); } }
Пояснение к коду
1) Создаем базовый sealed класс СontainerState(). Наследуем от него два наших состояния — AuthorizedState и NotAuthorizedState. Зачем нам нужен sealed класс и два наследника? В целом, просто для красоты и удобства. Это даёт нам возможность использовать в коде красивую switch-конструкцию и уменьшает количество кода в самом виджете.
2) Преобразуем Stateless виджет в Statefull. Нам ведь нужно где-то хранить состояние авторизации пользователя.
3) Создаём переменную state в стейтфул виджете. Если она является объектом AuthorizedState(), то показываем зелёный квадрат, если NotAuthorizedState — красный. За изменение этой переменной отвечает метод changeState(), вызываемый нажатием на FloatingActionButton().
Едем дальше
Если посмотреть на последний пример, то нетрудно заметить, что весь наш код свален в одну кучу. Внутри стейтфул виджета находятся методы, отвечающие как за отрисовку, так и за логику. Так делать ни в коем случае нельзя. При масштабировании этот код станет нечитаемым и крайне неудобным. Поэтому всегда старайся разделять код, выполняющий принципиально разные задачи, на логически связанные части
И как же отделить слой логики от презентационного слоя?
Тут нам и поможет стейт-менеджер. Сейчас мы сделаем небольшой апгрейд нашего примера, демонстрирующий одну из основных идей стейт-менеджмента.
Внешний вид остаётся тем же, но этот код будет намного легче поддерживать.
Как обычно, попробуй сначала сам понять, что происходит в коде, а потом читай пояснение.
Код классов состояний
sealed class ContainerState {} class AuthorizedState extends ContainerState {} class NotAuthorizedState extends ContainerState {}
Код стейт-менеджера + пояснение
class StateManager extends ChangeNotifier { ContainerState state = NotAuthorizedState(); void changeState() { if (state is AuthorizedState) { state = NotAuthorizedState(); } else { state = AuthorizedState(); } notifyListeners(); } } class StateManagerProvider extends InheritedNotifier<StateManager> { const StateManagerProvider({ required this.stateManager, super.key, required this.child, }) : super( child: child, notifier: stateManager, ); final StateManager stateManager; final Widget child; static StateManager of(BuildContext context) { return context .dependOnInheritedWidgetOfExactType<StateManagerProvider>()! .stateManager; } }
По сути только что мы создали примитивный стейт-менеджер.
В классе StateManager
теперь содержится и наш стейт, и вся логика изменения этого стейта. Мы полностью вынесли всю нашу логику в этот класс. Презентационный слой теперь никак не влияет на неё.
А класс StateManagerProvider
это просто инхерит, который мы пробросим в дерево. Зачем?
1) Это дарит нам возможность далее получать наш StateManager
в любой точке дерева.
2) За счёт использования InheritedNotifier
мы сможем в презентационном слое подписаться на изменения StateManager'а
. Это значит, что при изменении стейта, слой логики будет говорить презентационному слою — «Дружище, перерисуй экран!». И презентационный слой будет перерисовывать экран, учитывая новые данные.
Чтобы лучше понять, как работает InheritedNotifier, можешь посмотреть этот ролик — https://youtu.be/n_HLJUBkc48?feature=shared Да и вообще, все ролики на этом канале нужно обязательно посмотреть
Код презентационного слоя + пояснение
class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: StateManagerProvider( stateManager: StateManager(), child: const _View(), ), ); } } class _View extends StatelessWidget { const _View({ super.key, }); @override Widget build(BuildContext context) { final stateManager = StateManagerProvider.of(context); final state = stateManager.state; return Scaffold( floatingActionButton: FloatingActionButton( child: state is AuthorizedState ? const Text('log out') : const Text('log in'), onPressed: () { stateManager.changeState(); }, ), body: Center( child: SizedBox( height: 100, width: 100, child: switch (state) { AuthorizedState() => const ColoredBox(color: Colors.green), NotAuthorizedState() => const ColoredBox(color: Colors.red), }, ), ), ); } }
Чтобы обращаться к стейт-менеджеру, мы получаем его через context
. Также мы получаем state и отрисовываем нужный виджет.
Сразу обрати внимание, что в дереве теперь нет ни одного стейтфул виджета! На производительность, в целом, это не влияет, но смотрится чище. Это достигается за счёт того, что наш state
теперь хранится в StateManager'е
, а не в стейтфул виджете.
Но главное, чего мы достигли — наш презентационный слой полностью отделился от логического слоя и стал ещё тупее! Теперь мозг нашего примера — StateManager
, а презентационный слой просто показывает то, что приходит ему от нашего стейт-менеджера. Теперь, если мы захотим, мы можем вообще полностью поменять логику, ничего не меняя в презентационном слое, и всё будет работать!
Мы достигли того, что теперь наш StateManager
не только спрятан от презентационного слоя, но и полностью отвечает за то, какие данные и когда показывать. А презентационный слой отвечает только за то, как это выглядит. В этом и есть суть эффективного стейт-менеджмента.
Bloc
Теперь, когда ты понимаешь зачем нужен стейт-менеджмент, предлагаю освоить одну из самых популярных библиотек, облегчающую работу с управлением состояниями.
Для этого сначала необходимо научиться мыслить в пределах событий и состояний, на которых основывается bloc.
Как ты уже знаешь, всё, что пользователь видит на экране — это состояния (т.е. states).
И возникает естественный вопрос — как оперировать этими состояниями? Каким образом они сменяют друг друга?
На вопрос частично отвечает само название State Manager — менеджер состояний.
То есть у нас есть какой-то менеджер, которому мы даём команду, менеджер ее обрабатывает и выдаёт нам новый state. Командами, которые мы даём стейт-менеджеру, в библиотеке Bloc называются события (т.е. events
). Мы используем event'ы
в случаях, когда, например, пользователь нажал на кнопку и ожидает увидеть новый state.
Итого. Как выглядит в общих чертах вся схема работы со стейт менеджментом на Bloc?
добавляем event → bloc обрабатывает event → bloc возвращает state.
Смоделируем небольшой пример
Допустим, у нас есть два стейта — FirstState
и SesondState
. На экране мы показываем активный стейт и даем возможность поменять стейт на противоположный. Ниже схема, как будет работать наш Bloc. Ещё ниже будет демонстрация гифкой
Попробуем реализовать пример со схемы.
Мы ожидаем увидеть что-то подобное:
Код эвентов и стейтов + пояснение
sealed class Event {} class GoToFirstState extends Event {} class GoToSecondState extends Event {} sealed class State {} class FirstState extends State {} class SecondState extends State {}
Да, теперь в виде классов мы будем использовать не только стейты, но и эвенты. Во-первых это удобно, во-вторых — читаемо. Об остальных причинах пока не надо сильно задумываться
Код стейт-менеджера + пояснение
class AuthBloc extends Bloc<Event, State> { AuthBloc() : super(FirstState()) { on<GoToFirstState>((event, emit) { emit(FirstState()); }); on<GoToSecondState>((event, emit) { emit(SecondState()); }); } }
В конструктор с помощью super
мы передаём самый первый state
, который мы хотим показать. А в теле конструктора мы определяем функции для каждого нашего эвента. Теперь, когда в презентационной логике мы будем кидать event, bloc будет вызывать функцию, которую мы приготовили для этого эвента.
Код презентационного слоя
class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: BlocProvider( create: (BuildContext context) => AuthBloc(), child: const _View(), ), ); } } class _View extends StatelessWidget { const _View({ super.key, }); @override Widget build(BuildContext context) { final bloc = context.read<AuthBloc>(); return BlocBuilder<AuthBloc, State>( builder: (context, state) { return Scaffold( body: Center( child: switch (state) { FirstState() => StateScreen( text: '{FirstState}', textColor: Colors.green, buttonText: 'go to SecondState', buttonColor: Colors.red, onTap: () { bloc.add(GoToSecondState()); }, ), SecondState() => StateScreen( text: '{SecondState}', textColor: Colors.red, buttonText: 'go to FirstState', buttonColor: Colors.green, onTap: () { bloc.add(GoToFirstState()); }, ), }, ), ); }, ); } } class StateScreen extends StatelessWidget { const StateScreen({ super.key, required this.text, required this.buttonText, required this.onTap, required this.textColor, required this.buttonColor, }); final String text; final String buttonText; final Color textColor; final Color buttonColor; final VoidCallback onTap; @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( text, style: TextStyle(color: textColor, fontSize: 40), ), const SizedBox( height: 50, ), ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(buttonColor), ), onPressed: onTap, child: Text( buttonText, style: const TextStyle( color: Colors.white, ), ), ), ], ); } }
Рассмотрим подробнее сам bloc и процесс взаимодействия с ним.
Изначально мы находимся на экране FirstState
, так как при создании блока мы передаём его в super на этом участке кода:
class AuthBloc extends Bloc<Event, State> { AuthBloc() : super(FirstState()) {
Разберём весь наш путь при нажатии на кнопку.
-
Нажимаем на кнопку
go to SecondState
-
Вызывается метод
bloc.add(GoToSecondStateEvent)
-
Эвент
GoToSecondStateEvent
попадает в bloc и обрабатывается в теле конструктора. -
В блоке вызывается метод
emit(SecondState())
. Этот метод изменяет текущий стейт наSecondState()
и уведомляет об этом подписчиков. -
Виджет
BlocBuilder
, находящийся в презентационной логике, получает от блока уведомление о том, чтоstate
изменился и перерисовывает всё, что находится ниже по дереву. -
При перерисовке
switch
видит, что полученSecondState()
и поэтому рисуетStateScreen
с заголовком «{SecondState}» -
Мы видим новый экран с новым стейтом.
Этот пример, на самом деле, притянут за уши и не показывает всех возможностей, но, думаю, хорошо демонстрирует базовые основы работы с блоком. Дальше дело за тобой
Что делать дальше?
У блока есть большое количество виджетов, методов и фишек, которые необходимо научиться правильно использовать. Советую для начала почитать документацию — https://bloclibrary.dev/getting-started. Там, кстати, есть примеры, демонстрирующие эталонную работу с блоком.
Документация, само собой, на английском, но у себя в телеграмм-канале я разберу основные виджеты и расскажу как правильно их использовать, а также оставлю несколько ссылок на неплохие туториалы. Удачи!
тг: t.me/vanyakodit
ссылка на оригинал статьи https://habr.com/ru/articles/833054/
Добавить комментарий