Управление состоянием во Flutter: Provider, Riverpod или BLoC
Продвинутое4 минРедакция Innovialabs
Коротко
Provider подходит для небольших приложений, Riverpod — разумный выбор по умолчанию для большинства новых проектов, а BLoC оправдан в больших командах, где важны строгие правила и прозрачный поток событий.
Короткий ответ
Все три подхода решают одну задачу: держать данные вне виджетов и перерисовывать интерфейс, когда они меняются. Разница — в строгости и цене этой строгости.
- Provider — самый простой, обёртка над
InheritedWidget. Хорош для небольших приложений и прототипов. - Riverpod — от того же автора, но без зависимости от
BuildContext, с проверкой на этапе компиляции и удобными переопределениями в тестах. Разумный выбор по умолчанию. - BLoC — состояние меняется только через события или методы Cubit. Больше кода, зато предсказуемая архитектура для больших команд.
Локальное состояние одного виджета (раскрыт ли список, текст в поле) не нужно никуда выносить — для него достаточно setState.
Как это выглядит в коде
Одна и та же задача — корзина товаров — в трёх вариантах.
Provider с ChangeNotifier:
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
void add(Item item) {
_items.add(item);
notifyListeners();
}
}
// корень приложения
ChangeNotifierProvider(create: (_) => CartModel(), child: const App());
// в виджете
final count = context.watch<CartModel>().items.length;
Riverpod с Notifier:
final cartProvider =
NotifierProvider<CartNotifier, List<Item>>(CartNotifier.new);
class CartNotifier extends Notifier<List<Item>> {
@override
List<Item> build() => [];
void add(Item item) => state = [...state, item];
}
// в ConsumerWidget
final items = ref.watch(cartProvider);
ref.read(cartProvider.notifier).add(item);
BLoC в лёгкой форме — Cubit:
class CartCubit extends Cubit<List<Item>> {
CartCubit() : super(const []);
void add(Item item) => emit([...state, item]);
}
BlocBuilder<CartCubit, List<Item>>(
builder: (context, items) => Text("${items.length}"),
);
Полный Bloc добавляет классы событий (ItemAdded, ItemRemoved) и обработчики on<Event>. Это больше кода, но каждое изменение состояния имеет имя и его легко отследить в логах.
Сравнение по ключевым критериям
| Критерий | Provider | Riverpod | BLoC |
|---|---|---|---|
| Порог входа | Низкий | Средний | Средний–высокий |
| Шаблонный код | Минимум | Немного | Больше всего |
Зависимость от BuildContext | Да | Нет | Да (для доступа к блоку) |
| Ошибки при отсутствии провайдера | Во время выполнения | Ловятся раньше | Во время выполнения |
| Тестирование | Тестируется сам класс модели | ProviderContainer и переопределения | bloc_test, проверка последовательности состояний |
| Асинхронные данные | Вручную | Встроенные FutureProvider, AsyncValue | Через события и состояния загрузки |
| Масштаб команды | 1–3 разработчика | Любой, при договорённостях | Большие команды, строгий процесс |
Тестируемость
Provider. ChangeNotifier — обычный Dart-класс, его легко покрыть юнит-тестами. Сложнее с зависимостями: их приходится прокидывать через конструктор вручную.
Riverpod. Сильная сторона — переопределения. Можно подменить репозиторий фейком без изменения кода:
final container = ProviderContainer(
overrides: [repositoryProvider.overrideWithValue(FakeRepository())],
);
addTearDown(container.dispose);
container.read(cartProvider.notifier).add(item);
expect(container.read(cartProvider), [item]);
BLoC. Пакет bloc_test позволяет описать тест как «событие на входе — список состояний на выходе». Это особенно удобно для сложных сценариев: загрузка, ошибка, повтор.
Командная работа и масштаб
Главная проблема больших проектов — не выбор библиотеки, а разнобой. Если каждый разработчик пишет по-своему, любой подход превращается в хаос.
- BLoC навязывает структуру: событие, обработчик, состояние. Новичку проще понять, где искать логику.
- Riverpod гибче, поэтому команде нужно договориться: где лежат провайдеры, как называть, когда использовать
Notifier, а когдаFutureProvider. - Provider в большом проекте часто приводит к разрастающимся
ChangeNotifierс десятками полей и лишними перерисовками.
Как выбрать по размеру проекта
- Прототип, MVP, несколько экранов — Provider или Riverpod. Главное — быстро проверить идею.
- Продукт среднего размера, команда до нескольких человек — Riverpod: меньше кода, чем в BLoC, хорошие тесты и асинхронность из коробки.
- Крупное приложение, несколько команд, строгий код-ревью — BLoC или Riverpod с жёстким стайлгайдом. Решает то, что команда уже знает.
- Существующий проект — не переписывайте всё ради моды. Новые модули можно делать на новом подходе, старые переносить постепенно.
Частые ошибки
- Бизнес-логика в виджетах. Запросы к API и валидация должны жить в модели, notifier или bloc.
- Всё глобальное. Состояние формы на одном экране не нужно делать глобальным.
- Лишние перерисовки. Используйте
select(context.select,ref.watch(provider.select(...)),BlocSelector), чтобы виджет реагировал только на нужное поле. - Смешивание трёх подходов в одном проекте без явной причины.