Riverpod 3.0 vs BLoC 9: Które zarządzanie stanem wybrać w 2026 r
Wybór zarządzania państwowego we Flutter jest jedną z decyzji architektonicznych najbardziej wpływowy dla projektu. Zły wybór opłaca się przy refaktoryzacji drogie miesiące później. W 2026 roku rynek skonsolidował się wokół dwóch rozwiązań gotowe do produkcji: Riverpod 3.0 e BLoC 9. Dostawca — rozwiązanie historyczne — i należy go uważać za dziedzictwo dla nowych projektów.
Nie ma uniwersalnej odpowiedzi. Riverpod wyróżnia się pod względem DX (Developer Experience), bezpieczeństwo w czasie kompilacji i zmniejszony standard. BLoC utrzymuje przewagę w kontekstach korporacyjnych z dużymi zespołami, które wymagają sztywnej struktury i ścieżek audytu stanów i wyraźne rozdzielenie wymuszające testowalność. Ten przewodnik Ci to daje narzędzia do wyboru.
Czego się nauczysz
- Architektura i filozofia Riverpod 3.0 vs BLoC 9
- Macierz decyzyjna: której użyć w zależności od kontekstu
- Riverpod: AsyncNotifier, generowanie kodu, testowanie z nadpisywaniem
- BLoC: Cubit vs BLoC, zajęcia zamknięte, nawodnienie
- Migracja od dostawcy do Riverpod: wzorce i pułapki
- Porównanie wydajności: odbuduj szczegółowość
- Społeczność, ekosystem, dojrzał w 2026 roku
Riverpod 3.0: Filozofia i architektura
Riverpod został zaprojektowany, aby rozwiązać problemy strukturalne Providera: zależność od kontekstu Flutter, brak możliwości dostępu do dostawców z zewnątrz z drzewa widżetów, nie znaleziono błędów wykonawczych dla dostawców. Riverpod to naprawia rozwiązać te problemy, przenosząc dostawców na całym świecie, zapewniając bezpieczeństwo w czasie kompilacji.
// pubspec.yaml: dipendenze Riverpod 3.0
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
riverpod_generator: ^3.0.0
build_runner: ^2.4.0
riverpod_lint: ^2.0.0
// Struttura base: provider senza context
// Senza code generation (verboso ma esplicito)
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Provider semplice: valore computato
final userNameProvider = Provider<String>((ref) => 'Federico');
// StateProvider: stato semplice (counter, booleano, stringa)
final counterProvider = StateProvider<int>((ref) => 0);
// AsyncNotifierProvider: stato asincrono con loading/error/data
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
// build() e chiamato all'inizializzazione e ai rebuild
return ref.watch(userRepositoryProvider).fetchCurrentUser();
}
Future<void> updateProfile(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final updated = await ref.read(userRepositoryProvider).update(name);
return updated;
});
}
}
final userProvider = AsyncNotifierProvider<UserNotifier, User>(UserNotifier.new);
// Riverpod con code generation: zero boilerplate
// user_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_provider.g.dart'; // generato da build_runner
@riverpod
class UserController extends _$UserController {
@override
Future<User> build() async {
// ref disponibile come campo, nessun parametro al costruttore
return ref.watch(userRepositoryProvider).fetchCurrentUser();
}
Future<void> updateProfile(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return ref.read(userRepositoryProvider).update(name);
});
}
}
// @riverpod genera automaticamente userControllerProvider
// Equivalente a:
// final userControllerProvider = AsyncNotifierProvider<UserController, User>(
// UserController.new
// );
// Widget consumer: accede allo stato senza context.read()
class UserProfileWidget extends ConsumerWidget {
const UserProfileWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch: si ricostruisce quando lo stato cambia
final userAsync = ref.watch(userControllerProvider);
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error.toString()),
data: (user) => Text(user.name),
);
}
}
BLoC 9: Filozofia i architektura
BLoC (Business Logic Component) ściśle oddziela interfejs użytkownika od logiki biznesowej poprzez strumienie jednokierunkowe: interfejs użytkownika wysyła zdarzenia do BLoC, BLoC emituje stany, interfejs użytkownika odbudowuje się w odpowiedzi na stany. Chodzi o to wyraźne oddzielenie siły w dużych zespołach, w których testowalność i przewidywalność mają kluczowe znaczenie.
// pubspec.yaml: dipendenze BLoC 9
dependencies:
flutter_bloc: ^9.0.0
equatable: ^2.0.5 // Per comparazione stati
dev_dependencies:
bloc_test: ^9.0.0 // Testing utilities
// Cubit: versione semplificata di BLoC (senza eventi espliciti)
// Usalo quando lo stato ha transizioni semplici
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0); // stato iniziale
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}
// BLoC completo: con eventi e stati espliciti
// Sealed classes Dart 3.x per stati type-safe
// events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email, password;
LoginRequested(this.email, this.password);
}
class LogoutRequested extends AuthEvent {}
class TokenRefreshRequested extends AuthEvent {}
// states con sealed class
sealed class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
AuthAuthenticated(this.user);
}
class AuthUnauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _repo;
AuthBloc(this._repo) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final user = await _repo.login(event.email, event.password);
emit(AuthAuthenticated(user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
Future<void> _onLogoutRequested(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
await _repo.logout();
emit(AuthUnauthenticated());
}
}
Matryca decyzyjna: Riverpod kontra BLoC
| Kryterium | Riverpod 3.0 | BLoC 9 |
|---|---|---|
| Płyta główna | Niski (generowanie kodu) | Wysoki (zdarzenia + stany) |
| Bezpieczeństwo w czasie kompilacji | Doskonały | Dobry (zapieczętowane zajęcia) |
| Przetestowany | Znakomity (zastąpienie) | Znakomity (bloc_test) |
| Sztywna konstrukcja | Elastyczny | Bardzo sztywny |
| Świetne zespoły | Dobry | Doskonały |
| Krzywa uczenia się | Przeciętny | Wysoki |
| Obsługa asynchroniczna | Znakomicie (AsyncValue) | Dobry (ręczny) |
| Trwałość stanu | Zintegrowany (przechowywanie pod rzeką) | nawodniony_blok |
Kiedy wybrać Riverpoda
- Mały-średni zespół (1-5 programistów)
- Startup lub szybko rozwijający się produkt, w którym zmniejszenie szablonu przyspiesza rozwój
- Wiele żądań asynchronicznych, w których AsyncValue upraszcza ładowanie/błąd/dane
- Migracja z dostawcy (Family API)
- Gdy chcesz uzyskać dostęp do dostawców spoza drzewa widżetów (usługi, zadania w tle)
Kiedy wybrać BLoC
- Zespół korporacyjny (ponad 6 programistów) z rygorystycznymi przeglądami kodu
- Branże regulowane (fintech, opieka zdrowotna), które wymagają ścieżek audytu państwowego
- Projekt, w którym należy wymusić architektonicznie separację interfejsu użytkownika/logiki
- Zespół z doświadczeniem Redux/NgRx: filozofia i podobne
- Gdy chcesz, aby każde przejście stanu było wyraźne i możliwe do prześledzenia
Migracja od dostawcy do Riverpod
// Migrazione graduale: Provider -> Riverpod
// PRIMA: Provider (legacy)
// Provider: dipende dal context
final userProvider = ChangeNotifierProvider<UserModel>((ctx) => UserModel());
// Nel widget:
// final user = Provider.of<UserModel>(context);
// final user = context.watch<UserModel>();
// DOPO: Riverpod equivalente
// Nessuna dipendenza dal context
class UserNotifier extends Notifier<UserState> {
@override
UserState build() => UserState.initial();
void setName(String name) => state = state.copyWith(name: name);
}
final userNotifierProvider = NotifierProvider<UserNotifier, UserState>(
UserNotifier.new
);
// Strategia di migrazione graduale:
// 1. Aggiungi flutter_riverpod al progetto senza rimuovere Provider
// 2. Avvolgi l'app con ProviderScope (necessario per Riverpod)
// ProviderScope puo coesistere con MultiProvider
// 3. Migra un provider alla volta partendo dai leaf (quelli senza dipendenze)
// 4. Usa ref.read(oldProviderViaAdapter) come bridge temporaneo
// 5. Rimuovi Provider quando tutti i consumer sono migrati
Wnioski
W 2026 roku oba rozwiązania są dojrzałe i sprawdzone w boju. Wybór zależy z kontekstu: W przypadku większości projektów Riverpod 3.0 oferuje to, co najlepsze DX z mniejszą ilością kodu do napisania i utrzymania. Dla zespołów korporacyjnych z wymaganiami ścieżek audytu i sztywnej struktury, BLoC pozostaje niedościgniony. Ważne tj wybieraj i bądź konsekwentny: unikaj mieszania dwóch wzorów w ten sam projekt bez jasnej strategii.







