Перейти к содержанию
Innovialabs

Управление состоянием во Flutter: Provider, Riverpod или BLoC

Продвинутое4 мин

Коротко

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>. Это больше кода, но каждое изменение состояния имеет имя и его легко отследить в логах.

Сравнение по ключевым критериям

КритерийProviderRiverpodBLoC
Порог входаНизкийСреднийСредний–высокий
Шаблонный кодМинимумНемногоБольше всего
Зависимость от 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 с десятками полей и лишними перерисовками.

Как выбрать по размеру проекта

  1. Прототип, MVP, несколько экранов — Provider или Riverpod. Главное — быстро проверить идею.
  2. Продукт среднего размера, команда до нескольких человек — Riverpod: меньше кода, чем в BLoC, хорошие тесты и асинхронность из коробки.
  3. Крупное приложение, несколько команд, строгий код-ревью — BLoC или Riverpod с жёстким стайлгайдом. Решает то, что команда уже знает.
  4. Существующий проект — не переписывайте всё ради моды. Новые модули можно делать на новом подходе, старые переносить постепенно.

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

  • Бизнес-логика в виджетах. Запросы к API и валидация должны жить в модели, notifier или bloc.
  • Всё глобальное. Состояние формы на одном экране не нужно делать глобальным.
  • Лишние перерисовки. Используйте select (context.select, ref.watch(provider.select(...)), BlocSelector), чтобы виджет реагировал только на нужное поле.
  • Смешивание трёх подходов в одном проекте без явной причины.

Частые вопросы

Технически да, но это усложняет поддержку: два способа внедрения зависимостей и две модели мышления. Если причина есть — например, миграция — зафиксируйте, какой подход используется для нового кода.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.