Урок по треку Flutter и Dart 8 мин

Состояние: setState, Provider, Riverpod, BLoC

setState, InheritedWidget, Provider, Riverpod и BLoC: когда что выбрать.

Вопрос «какой стейт-менеджер вы используете и почему» звучит на каждом Flutter-собесе. Интервьюеру важно не название пакета, а понимание: где живёт состояние, кто его меняет, что перестраивается и как это тестировать.

Локальное и общее состояние

  • Локальное (эфемерное): открыт ли раскрывающийся блок, текущая вкладка, ввод в поле. Живёт в State и меняется через setState. Тащить его в глобальное хранилище не нужно.
  • Общее (состояние приложения): пользователь, корзина, настройки, кеш данных. Нужно нескольким экранам, должно переживать уход с экрана, его хочется тестировать без UI.

InheritedWidget: фундамент

Все популярные решения построены на InheritedWidget. Он хранит данные и позволяет потомку найти его за O(1) через context.dependOnInheritedWidgetOfExactType<T>(). Потомок при этом подписывается: когда InheritedWidget заменяется новым и updateShouldNotify вернул true, у всех зависимых вызываются didChangeDependencies и build. Так работают Theme.of, MediaQuery.of, Navigator.of.

Provider

Обёртка над InheritedWidget с удобным API, долго была рекомендацией команды Flutter.

ChangeNotifierProvider(create: (_) => Cart(), child: App());

context.watch<Cart>();          // подписаться: перестроиться при notifyListeners
context.read<Cart>().add(item); // прочитать без подписки: в обработчиках
context.select<Cart, int>((c) => c.count); // подписаться на часть

watch в обработчике нажатия и read в build это две частые ошибки: первое бессмысленно, второе даёт экран, который не обновляется.

Riverpod

Автор тот же, что у Provider, но провайдеры объявляются глобально и не зависят от дерева виджетов, поэтому нет ProviderNotFoundException, а зависимости между провайдерами проверяются компилятором.

final userProvider = FutureProvider<User>((ref) async {
  final api = ref.watch(apiProvider);
  return api.me();
});

class Profile extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);
    return user.when(
      data: (u) => Text(u.name),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('$e'),
    );
  }
}

Сильные стороны: кеширование и автоочистка (autoDispose), параметры через family, удобная подмена провайдеров в тестах (ProviderScope(overrides: [...])). Состояние с логикой описывают через Notifier/AsyncNotifier.

BLoC

Business Logic Component: виджет отправляет события, блок превращает их в поток состояний. Пакет flutter_bloc.

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

Cubit проще: методы вместо событий. Bloc с событиями даёт трассировку и трансформацию событий (debounce, droppable, restartable), что удобно для поиска и форм. В UI: BlocBuilder перестраивает, BlocListener делает побочные эффекты (навигация, снекбар), BlocConsumer объединяет. buildWhen и listenWhen отсекают лишнее.

Плюсы BLoC: строгая однонаправленность, предсказуемость, тесты через blocTest. Минус: много шаблонного кода для простых экранов.

Как выбрать

СитуацияВыбор
Локальное UI-состояниеsetState, ValueNotifier
Небольшое приложение, простая логикаProvider
Много асинхронных данных, кеш, зависимостиRiverpod
Большая команда, строгие правила, сложные сценарииBLoC

Состояние делают неизменяемым (часто freezed), чтобы сравнение == было корректным, а перестроения предсказуемыми. Мутация списка внутри состояния с emit(state) того же объекта не вызовет перестроения: Bloc сравнивает новое состояние со старым.

Частые ошибки

  • Всё глобально, даже флаг раскрытия карточки.
  • Изменяемое состояние, мутация вместо нового объекта.
  • Побочные эффекты (навигация) внутри build или BlocBuilder вместо BlocListener.
  • Один огромный провайдер на всё приложение: перестраивается всё.

Что спросят на собесе

  • Как работает InheritedWidget и почему на нём всё построено.
  • watch, read, select в Provider.
  • Чем Riverpod отличается от Provider.
  • Bloc против Cubit, BlocBuilder против BlocListener.
  • Как вы выбираете решение для нового проекта.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.