Вопрос «какой стейт-менеджер вы используете и почему» звучит на каждом 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.- Как вы выбираете решение для нового проекта.