Start Debugging

Provider vs Riverpod vs Bloc для управления состоянием во Flutter в 2026

Выбирайте Riverpod для большинства новых приложений Flutter в 2026. Берите Bloc, когда большой команде нужна строгая событийная структура, а Provider оставьте только для устаревшего кода.

Если вы начинаете новое приложение Flutter в 2026 году и не можете выбрать между Provider, Riverpod и Bloc, короткий ответ - Riverpod. С Riverpod 3.3.1 (линейка 3.0 вышла 2025-09-10) он безопасен на этапе компиляции, тестируется без BuildContext, а путь с генерацией кода устраняет почти весь boilerplate, который раньше был главным аргументом против него. Берите Bloc (flutter_bloc 9.1.1), когда у вас большая команда, которой выгоден строгий событийный контракт и отслеживаемая история состояния. Оставьте Provider (6.1.5) только если он уже есть в вашей базе кода или вы объясняете кому-то лежащую в основе модель InheritedWidget. Все примеры здесь используют Flutter 3.44 и Dart 3.12.

Эти три пакета - не одинаковый тип инструмента

Перед сравнением полезно увидеть, что эти библиотеки решают пересекающиеся, но разные задачи.

Provider - это тонкая, хорошо сделанная обёртка над собственным InheritedWidget Flutter. Он выполняет внедрение зависимостей и распространение перестроений, и по сути это всё. Класс состояния обычно представляет собой ChangeNotifier, который вы пишете вручную. Это пакет, к которому обратилась официальная документация Flutter, когда ей понадобился обучающий пример, поэтому так много туториалов используют его.

Riverpod - это то, что автор Provider построил следующим, специально чтобы исправить структурные проблемы Provider: ProviderNotFoundException во время выполнения, зависимость от позиции виджета в дереве и невозможность читать состояние из чистого Dart. Провайдеры Riverpod живут вне дерева виджетов, поэтому доступны откуда угодно и разрешаются на этапе компиляции.

Bloc - это сначала паттерн, а потом пакет. Он подталкивает вас моделировать каждое изменение как явное событие, которое поступает в компонент и порождает новое неизменяемое состояние. Эта церемония и есть суть: в большой команде принудительный конвейер Event -> Bloc -> State делает поведение предсказуемым и проверяемым на ревью.

Матрица возможностей

ВозможностьProvider 6.1.5Riverpod 3.3.1Bloc 9.1.1
Ментальная модельInheritedWidget + ChangeNotifierПровайдеры вне дереваСобытийная, неизменяемые состояния
Безопасность на этапе компиляцииНет (поиск во время выполнения)ДаДа
Нужен BuildContext для чтенияДаНетНет (через context.read или напрямую)
BoilerplateНизкийНизкий с codegenВысокий
ТестируемостьТребует прокачки виджетаЧистый Dart, без дерева виджетовЧистый Dart, помощники bloc_test
Async- / состояние загрузкиВручнуюAsyncValue, встроеноСостояния вручную или emit
Автоповтор при сбояхНетДа (с 3.0)Нет
Отслеживаемость состоянияСлабаяСредняяСильная (каждый переход наблюдаем)
Кривая обученияПологаяУмереннаяКрутая
Лучше всего подходитLegacy, туториалыБольшинство новых приложенийБольшие команды, сложные потоки

Самая важная строка - “безопасность на этапе компиляции”. Неправильно настроенный Provider бросает ProviderNotFoundException во время выполнения, часто только на том экране, где его не хватает. Riverpod и Bloc выявляют этот класс ошибок до запуска приложения.

Один и тот же счётчик во всех трёх

Счётчик достаточно мал, чтобы напрямую сравнить эргономику. Обратите внимание, сколько кода нужно каждому и где живёт состояние.

Provider

// Flutter 3.44, provider 6.1.5
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

// Register it above the widgets that need it.
ChangeNotifierProvider(
  create: (_) => CounterModel(),
  child: const CounterPage(),
);

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.watch<CounterModel>().count;
    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Обратите внимание на зависимость от context. Если CounterPage отрендерен без ChangeNotifierProvider над ним, context.watch<CounterModel>() бросает исключение во время выполнения.

Riverpod

// Flutter 3.44, flutter_riverpod 3.3.1, riverpod_annotation
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
}

class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

counterProvider генерируется и доступен глобально. Нет позиции в дереве, которую можно перепутать, а ref разрешается на этапе компиляции. Оберните приложение один раз в ProviderScope, и готово.

Bloc

// Flutter 3.44, flutter_bloc 9.1.1, equatable
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// Events
sealed class CounterEvent {}
class Increment extends CounterEvent {}

// Bloc: Event in, int state out.
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: Builder(
        builder: (context) => Scaffold(
          body: Center(
            child: BlocBuilder<CounterBloc, int>(
              builder: (context, count) => Text('$count'),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => context.read<CounterBloc>().add(Increment()),
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}

Bloc самый многословный для счётчика, и это сравнение к нему несправедливо: ценность Bloc проявляется, когда событий двадцать, а состояний десять, а не одно. Событие Increment - это запись в истории вашего приложения. С подключённым BlocObserver вы можете логировать каждый переход, а это именно то, что нужно при отладке сложного экрана.

Когда выбирать Riverpod

Одна оговорка: в Riverpod 3.0 StateProvider, StateNotifierProvider и ChangeNotifierProvider перенесены в package:riverpod/legacy.dart. Новый код должен использовать классы Notifier и AsyncNotifier, показанные выше, в идеале с генерацией кода через @riverpod.

Когда выбирать Bloc

Когда выбирать Provider

Чем Provider не должен быть в 2026 году, так это вариантом по умолчанию для нового нетривиального приложения. Модель поиска во время выполнения - это ровно та проблема, для устранения которой был создан Riverpod.

Подводные камни, которые выбирают за вас

Некоторые ограничения перевешивают личные предпочтения.

Чтение состояния из кода вне виджетов. Если в вашей архитектуре есть слой сервиса или репозитория, который должен читать состояние приложения напрямую, Provider фактически выбывает. Ему нужен BuildContext. Riverpod и Bloc оба позволяют читать состояние из чистого Dart, что обычно само решает выбор.

Размер команды и культура ревью. В одиночном проекте или маленькой команде церемония Bloc - это трение с малой отдачей, и Riverpod выигрывает по скорости. В команде из 15 человек, где согласованность между функциями важнее количества строк кода, жёсткость Bloc - это преимущество, а не издержка.

Дисциплина неизменяемого состояния. И Bloc, и современный Riverpod подталкивают вас к неизменяемым объектам состояния. Если ваша команда комфортно работает с sealed-классами и равенством по значению (см. Dart records vs классы Freezed про варианты моделирования), подходят оба. Если у вас большая база кода на изменяемых объектах ChangeNotifier, самый дешёвый путь - возможно, остаться на Provider, пока какой-то функции действительно не понадобится больше.

Существующие async-паттерны. AsyncValue от Riverpod - это способ с наименьшими усилиями отрисовать загрузку, данные и ошибку из единого источника истины. Если ваши экраны - это в основном async-загрузки данных, одного этого уже достаточно как веской причины выбрать его.

Рекомендация, ещё раз

Для большинства новых приложений Flutter в 2026 году используйте Riverpod 3.3.1 с генерацией кода. Вы получаете безопасность на этапе компиляции, чтение без контекста, первоклассный async и функции устойчивости из 3.0, при затратах на boilerplate, которые codegen приближает к Provider.

Выбирайте Bloc 9.1.1, когда принудительная событийная структура и полностью отслеживаемая история состояния значат для вашей команды больше, чем краткость, что чаще всего верно для больших команд и сложных потоков. Используйте Cubit внутри приложения на Bloc для экранов, которым не нужны полноценные события.

Оставьте Provider 6.1.5 для legacy-приложений, обучения и тривиальных экранов, но не берите его по умолчанию в новом нетривиальном проекте. Решающий вопрос редко звучит как “у кого самый красивый счётчик”, он звучит как “читаю ли я состояние вне виджетов, насколько велика моя команда и сколько у меня async?”. Ответьте на эти три, и выбор обычно сделается сам собой. Если вы взвешиваете Flutter против совсем других стеков, наше сравнение Flutter vs React Native vs MAUI отдаляет камеру на уровень выше. И что бы вы ни выбрали, не забудьте освобождать ваши контроллеры, потому что ни одна библиотека управления состоянием не подчистит их за вас.

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад