Flutter BLoC vs Riverpod у 2025: практичний фреймворк для вибору
Після запуску кількох Flutter-застосунків у продакшн з BLoC і Riverpod — ось практичний фреймворк для вибору між ними на основі реальних компромісів, з якими ми стикались у роботі.
Коротка відповідь, яку ніхто не хоче чути
Жодне з рішень не є universally кращим. Відповідь залежить від команди, складності проєкту та того, наскільки серйозно ви ставитеся до тестування. Якщо потрібен один рядок: BLoC — для складної бізнес-логіки з кількома інженерами, Riverpod — для менших застосунків або команд, які обирають менше церемоній.
Решта статті пояснює чому.
Що таке BLoC насправді
BLoC — Business Logic Component. Це патерн управління станом, побудований навколо стрімів і розділення бізнес-логіки від UI. Пакет flutter_bloc забезпечує реалізацію.
Основна ментальна модель: UI відправляє Events, BLoC обробляє їх і емітує States, а віджети реагують на зміни стану.
// Event
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
// State
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
AuthAuthenticated(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}Переваги BLoC:
- Явна event-driven архітектура. Кожна зміна стану має іменовану причину.
- Відмінна тестованість. Можна юніт-тестувати BLoC без торкання UI.
- Чітке розділення відповідальностей. UI не знає нічого про те, як змінюється стан.
- Пакет
flutter_bloc_testспрощує тестування стрімів.
Недоліки BLoC:
- Значний boilerplate, особливо для простого стану.
- Event-класи збільшують кількість файлів і когнітивне навантаження.
- Cubit існує для зменшення boilerplate, але команди часто непослідовно поєднують BLoC і Cubit.
Що таке Riverpod насправді
Riverpod — повне переосмислення Provider. Він compile-safe, підтримує генерацію коду (riverpod_generator) і використовує концепцію Providers — типізованих реактивних сховищ стану.
// Простий async provider для завантаження профілю
@riverpod
Future<UserProfile> userProfile(UserProfileRef ref, String userId) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchProfile(userId);
}
// У віджеті
class ProfilePage extends ConsumerWidget {
final String userId;
const ProfilePage({required this.userId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(userProfileProvider(userId));
return profile.when(
data: (user) => ProfileContent(user: user),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorView(message: e.toString()),
);
}
}Переваги Riverpod:
- Менше boilerplate для типових патернів (завантажити дані, трансформувати, відобразити).
- Генерація коду через анотацію
@riverpodбере на себе рутинні частини. - Providers є composable — один provider може залежати від іншого.
- Кращий support для обчислюваного/похідного стану без явних маперів.
ref.invalidate()іref.refresh()дають явний контроль над кешуванням.
Реальні виробничі сценарії
Сценарій 1: Форма з валідацією та відправкою
Тут BLoC сяє. Є явні events (FieldChanged, StepAdvanced, FormSubmitted) і явні states (FormEditing, FormSubmitting, FormSuccess, FormError).
Переможець: BLoC
Сценарій 2: Завантаження даних з кешуванням
Профіль користувача, який завантажується раз і доступний на кількох екранах. З Riverpod — один provider, будь-який екран викликає ref.watch. Кешування автоматичне. Інвалідація при logout — ref.invalidate(userProfileProvider).
Переможець: Riverpod
Чинник команди
Це найнедооцінена змінна.
Якщо у команді 3+ Flutter-розробників і потрібні чіткі межі відповідальності — детальність BLoC стає перевагою. Кожен файл іменований, кожен event явний. Нові розробники можуть читати BLoC і розуміти, що робить фіча, не запускаючи застосунок.
Якщо ви соло або невелика 2-особова команда і рухаєтесь швидко — Riverpod знімає достатньо церемоній, щоб суттєво прискорити розробку.
Наш поточний default у Codevia
Ми використовуємо BLoC (варіант Cubit) для складних, насичених бізнес-логікою фіч і Riverpod для простого завантаження даних та обчислюваного стану.
Ми дотримуємось суворого правила: один BlocConsumer або BlocListener на кубіт на сторінку. Це запобігає антипатерну, коли один кубіт має listeners розкидані по вкладених віджетах.
Фреймворк прийняття рішень
- Чи є у фічі складна бізнес-логіка з кількома джерелами подій? → BLoC
- Чи є стан переважно результатом async-операції? → Riverpod
- Велика команда (4+ людей) з потребою чіткого ownership? → BLoC
- Невелика команда або MVP? → Riverpod
- Є вже один варіант у кодобазі? → Використовуйте його послідовно
Висновок
Не обирайте на основі зірочок на GitHub чи виступів на конференціях. Обирайте на основі:
- Розміру та досвіду команди
- Складності фічей і щільності подій
- Наскільки ви цінуєте явну причинність проти лаконічного коду
Якщо сумніваєтесь — починайте з Riverpod. Якщо стикнетесь з труднощами у відстеженні переходів стану — вводьте BLoC для конкретних фіч. Можна поєднувати на рівні фіч — але не на рівні віджетів.