Von provider zu Riverpod in Flutter migrieren (provider 6.1.5 zu Riverpod 3.x)
Eine schrittweise Migration vom provider-Paket zu Riverpod 3.x in einer echten Flutter-App: ChangeNotifierProvider zu Notifier, MultiProvider zu ProviderScope, context.watch zu ref.watch, ProxyProvider zu ref.watch-Komposition, plus die Gleichheits- und Lifecycle-Fallstricke, die Sie erwischen. Getestet mit Flutter 3.27.1, Dart 3.11, provider 6.1.5, flutter_riverpod 3.3.1.
Die Kurzfassung: Fügen Sie flutter_riverpod neben provider hinzu, kapseln Sie Ihre App in einen ProviderScope statt in einen MultiProvider, und migrieren Sie eine Funktion nach der anderen, beginnend bei den Blättern Ihres Abhängigkeitsbaums. Jeder ChangeNotifier wird zu einem Notifier (oder AsyncNotifier für asynchrone Arbeit), context.watch<T>() wird zu ref.watch(myProvider), Provider.of und context.read werden zu ref.read, und jeder ProxyProvider kollabiert zu einem einfachen ref.watch eines anderen Providers. Eine kleine bis mittelgroße App ist ein Ein- bis Dreitagesprojekt; der knifflige Teil ist nicht die Syntax, sondern dass Riverpod den State per Gleichheit vergleicht und Provider anders am Leben hält als provider es tut. Getestet mit Flutter 3.27.1, Dart 3.11, provider 6.1.5, flutter_riverpod 3.3.1, riverpod_annotation 2.6.1 und riverpod_generator 2.6.5.
Das provider-Paket (derzeit 6.1.5) ist seit 2019 die Standardantwort für Flutter-State-Management, und es funktioniert nach wie vor. Aber sein Autor, Remi Rousselet, hat Riverpod gezielt geschrieben, um die strukturellen Probleme von provider zu beheben: State, der über BuildContext gelesen wird, bedeutet eine ProviderNotFoundException zur Laufzeit statt eines Compile-Fehlers, ProxyProvider-Verschachtelung wird ab zwei Abhängigkeiten unleserlich, und Sie können nicht zwei Provider desselben Typs ohne ValueKey-Verrenkungen haben. Riverpod behält das mentale Modell (ein Graph von Objekten, die Widgets neu aufbauen, wenn sie sich ändern) und entfernt die BuildContext-Kopplung. Dieser Leitfaden ist die mechanische, blatt-zuerst durchgeführte Migration, die kein Neuschreiben erfordert.
Warum von provider weg migrieren
- Compile-Zeit-Sicherheit statt
ProviderNotFoundException. Inproviderwirft das Lesen eines Typs, der nicht über Ihnen im Baum liegt, zur Laufzeit. In Riverpod sind Provider Top-Level-Globals, sodass ein Tippfehler ein Compile-Fehler ist und es nichts zu “finden” gibt. - Keine
MultiProvider-Pyramide mehr. Riverpod hat keinen Provider-Baum zusammenzusetzen. EinProviderScopean der Wurzel ersetzt die gesamteMultiProvider(providers: [...])-Liste, und Abhängigkeiten zwischen Providern werden mitref.watchausgedrückt, nicht durch die Verschachtelungsreihenfolge. - Zwei Provider desselben Typs, kostenlos.
providerindiziert alles nach Typ, sodass zweiChangeNotifierProvider<CartModel>kollidieren. Riverpod indiziert nach dem Provider-Objekt, also ist das kein Problem. - Auto-Dispose und Family, die tatsächlich komponieren. Riverpod gibt Ihnen
autoDisposeund parametrisierte (family) Provider als erstklassige Funktionen, dieprovidernur mit manuellemChangeNotifierProvider.valueund Schlüsselverwaltung annähert.
Was bricht
| Bereich | Änderung | Schweregrad |
|---|---|---|
| Root-Verdrahtung | MultiProvider ersetzt durch einen einzigen ProviderScope | mittel |
| Reads | context.watch<T>() / Provider.of<T>(context) ersetzt durch ref.watch / ref.read | hoch |
| Notifier | ChangeNotifier + notifyListeners() ersetzt durch Notifier + State-Neuzuweisung | hoch |
| Rebuild-Semantik | Riverpod vergleicht State per ==; In-Place-Mutation baut nicht mehr neu auf | hoch |
| Komposition | ProxyProvider ersetzt durch ref.watch der Abhängigkeit | mittel |
| Widgets | StatelessWidget / StatefulWidget werden zu ConsumerWidget / ConsumerStatefulWidget | mittel |
| Lifecycle | provider verwirft beim Entfernen aus dem Baum; Riverpod behält State bis autoDispose | mittel |
Die zwei hoch-Zeilen in den Bereichen Rebuild und Notifier sind die, in denen Teams Zeit verlieren. Alles andere ist Suchen-und-Ersetzen.
Pre-Flight-Checkliste
- Flutter 3.27.1 / Dart 3.11 (oder neuer) installiert:
flutter --version. - Ein sauberer
git-Arbeitsbaum und ein Branch, den Sie wegwerfen können. - Ein Inventar jedes Providers, den Sie heute registrieren. Durchsuchen Sie Ihre Codebasis:
grep -rn "ChangeNotifierProvider\|ProxyProvider\|FutureProvider\|StreamProvider\|Provider.of\|context.watch\|context.read" lib/. - Eine Notiz neben jedem davon, ob etwas davon abhängt. Migrieren Sie zuerst die Dinge, von denen nichts abhängt.
- Eine funktionierende Testsuite, auch eine dünne. Sie werden sie nach jedem Schritt ausführen.
Migrationsschritte
-
Fügen Sie Riverpod neben provider in
pubspec.yamlhinzu. Entfernen Sieprovidernoch nicht. Beide Pakete koexistieren, weil sie getrennte Bäume besitzen; ein gegebenes Stück State hat zu jedem Zeitpunkt genau einen Besitzer, also migrieren Sie pro Funktion, nicht pro Typ.# pubspec.yaml. Flutter 3.27.1, Dart 3.11. dependencies: flutter: sdk: flutter provider: ^6.1.5 # keep until migration is done flutter_riverpod: ^3.3.1 riverpod_annotation: ^2.6.1 dev_dependencies: build_runner: ^2.4.13 riverpod_generator: ^2.6.5 custom_lint: ^0.7.0 riverpod_lint: ^2.6.5Prüfen:
flutter pub getlöst ohne Versionskonflikte auf. -
Kapseln Sie die App-Wurzel in
ProviderScopeund behalten SieMultiProvidervorerst darin.ProviderScopeist der Ort, an dem Riverpod den gesamten Provider-State speichert. Es ist keine Liste von Providern, es ist eine einzelne Grenze. Lassen Sie Ihren bestehendenMultiProviderdarunter, damit nicht migrierte Screens weiter funktionieren.// lib/main.dart, Flutter 3.27.1 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:provider/provider.dart'; void main() { runApp( ProviderScope( // Riverpod root child: MultiProvider( // legacy, shrinks as you migrate providers: [ ChangeNotifierProvider(create: (_) => CartModel()), ChangeNotifierProvider(create: (_) => AuthModel()), ], child: const MyApp(), ), ), ); }Prüfen: Die App kompiliert und läuft weiterhin identisch. Es hat sich noch nichts bewegt.
-
Konvertieren Sie einen Blatt-
ChangeNotifierin einenNotifier. Wählen Sie ein Modell, von dem nichts anderes abhängt. Inprovidermutieren Sie ein Feld und rufennotifyListeners()auf. In Riverpod gibtbuild()den initialen State zurück, und Sie weisenstateneu zu, um zu benachrichtigen. Es gibt keinnotifyListeners().// Before: provider 6.1.5 class CartModel extends ChangeNotifier { final List<Item> _items = []; List<Item> get items => List.unmodifiable(_items); void add(Item item) { _items.add(item); notifyListeners(); } }// After: flutter_riverpod 3.3.1, code generation import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'cart_model.g.dart'; @riverpod class Cart extends _$Cart { @override List<Item> build() => const []; void add(Item item) { state = [...state, item]; // new list, not state.add(...) } }Führen Sie
dart run build_runner build --delete-conflicting-outputsaus, umcartProviderzu generieren. Prüfen: Der Generator erzeugtcart_model.g.dartohne Fehler. -
Stellen Sie den Screen, der ihn konsumiert, auf ein
ConsumerWidgetum.StatelessWidgetwird zuConsumerWidget, undbuilderhält einWidgetRef ref.context.watch<CartModel>()wird zuref.watch(cartProvider). Für einen Methodenaufruf wirdcontext.read<CartModel>().add(x)zuref.read(cartProvider.notifier).add(x).// Before class CartView extends StatelessWidget { @override Widget build(BuildContext context) { final items = context.watch<CartModel>().items; return Column(children: [ for (final i in items) Text(i.name), ElevatedButton( onPressed: () => context.read<CartModel>().add(Item('pen')), child: const Text('Add'), ), ]); } }// After class CartView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final items = ref.watch(cartProvider); return Column(children: [ for (final i in items) Text(i.name), ElevatedButton( onPressed: () => ref.read(cartProvider.notifier).add(Item('pen')), child: const Text('Add'), ), ]); } }Wenn das Widget bereits eigenen State hatte, verwenden Sie
ConsumerStatefulWidgetplusConsumerState, wobeirefals Feld verfügbar ist. Entfernen Sie die ZeileChangeNotifierProvider(create: (_) => CartModel())ausMultiProvider. Prüfen: Der Screen verhält sich gleich, und dieMultiProvider-Liste ist um eins kürzer. -
Ersetzen Sie
ProxyProviderdurchref.watch-Komposition. Das ist der Schritt, der den meisten Code löscht. EinProxyProvider, der B aus A baut, wird zu einem Provider, der einfach A beobachtet.// Before: ProxyProvider wiring ProxyProvider<AuthModel, ApiClient>( update: (_, auth, __) => ApiClient(token: auth.token), ),// After: a provider that watches its dependency @riverpod ApiClient apiClient(ApiClientRef ref) { final token = ref.watch(authProvider.select((a) => a.token)); return ApiClient(token: token); }ref.watch(...select(...))ist der direkte Ersatz fürproviderscontext.select, und es bedeutet, dassapiClientnur dann neu aufgebaut wird, wenn sichtokenändert, nicht bei jedemAuthModel-Update. Prüfen: Abhängige Widgets bauen neu auf, wenn sich der vorgelagerte Provider ändert. -
Migrieren Sie
FutureProviderundStreamProviderzu ihren Riverpod-Äquivalenten. Die Namen sind gleich; nur die Verdrahtung unterscheidet sich. Einprovider-FutureProviderwird mitcontext.watch<AsyncSnapshot>-artiger Verdrahtung gelesen; der Riverpod-Provider gibt einAsyncValue<T>zurück, auf das Sie direkt verzweigen.// After: flutter_riverpod 3.3.1 @riverpod Future<User> currentUser(CurrentUserRef ref) { return ref.watch(apiClientProvider).fetchUser(); } // in a ConsumerWidget final userAsync = ref.watch(currentUserProvider); return userAsync.when( data: (user) => Text(user.name), loading: () => const CircularProgressIndicator(), error: (e, _) => Text('Failed: $e'), );Prüfen: Lade- und Fehlerzustände werden ohne manuelle
bool isLoading-Flags gerendert. Mehr zu diesem Muster finden Sie im verlinkten AsyncValue-Beitrag weiter unten. -
Löschen Sie die
provider-Abhängigkeit. SobaldMultiProviderleer ist, entfernen Sie es ausmain.dart, lassen dannprovider: ^6.1.5auspubspec.yamlfallen und führenflutter pub getaus. Der Compiler wird alle verbleibendencontext.watch/context.read/Provider.of-Aufrufe markieren. Prüfen: Das Projekt kompiliert mit null Referenzen aufpackage:provider.
Verifizierung
Führen Sie diese Checkliste nach dem letzten Schritt aus, nicht nur ganz am Ende:
flutter analyzemeldet keine Fehler und keineriverpod_lint-Warnungen.dart run build_runner build --delete-conflicting-outputsläuft sauber durch.flutter testbesteht. Riverpod-Tests verwendenProviderContainer(oderProviderContainer.test()in 3.x) undcontainer.read(provider)und ersetzen Ihre altenChangeNotifier-Unit-Tests.- Ein manueller Smoke-Durchlauf: Jeder migrierte Screen baut bei State-Änderung weiterhin neu auf, und kein Screen wirft
ProviderNotFoundException(es sollte per Konstruktion keine mehr geben). grep -rn "package:provider" lib/gibt nichts zurück.
Rollback-Plan
Diese Migration ist pro Funktion umkehrbar, gerade weil beide Pakete koexistieren. Wenn sich ein migrierter Screen fehlerhaft verhält, machen Sie den Commit dieses Screens rückgängig: Setzen Sie die ChangeNotifierProvider-Zeile wieder in MultiProvider ein, stellen Sie die ChangeNotifier-Klasse wieder her, und ändern Sie das Widget zurück auf StatelessWidget. Weil Sie blatt-zuerst und eine Funktion pro Commit migriert haben, berührt kein Rollback mehr als einen Screen. Löschen Sie provider nicht aus pubspec.yaml (Schritt 7), bis Sie zuversichtlich sind, denn das ist die einzige Einbahnstraße in der Sequenz.
Fallstricke, auf die wir gestoßen sind
In-Place-Mutation hört auf, neu aufzubauen. Das ist die Überraschung Nummer eins. In provider funktioniert _items.add(x); notifyListeners(), weil Sie die Benachrichtigung kontrollieren. In einem Riverpod-Notifier baut das Framework nur dann neu auf, wenn state ein Wert zugewiesen wird, der nicht == zum alten ist. state.add(x) mutiert dieselbe Liste, die Referenz ist unverändert, und es wird nichts neu aufgebaut. Weisen Sie immer eine neue Collection zu: state = [...state, x]. Dasselbe gilt für Modellobjekte, weshalb unveränderlicher State (Records, copyWith oder eine freezed-Klasse) natürlich zu Riverpod passt.
Provider werden nicht verworfen, wenn das Widget den Baum verlässt. Ein provider-ChangeNotifierProvider wird verworfen, wenn sein Subtree entfernt wird. Ein Riverpod-Provider behält standardmäßig seinen State für die Lebensdauer des ProviderScope. Wenn Sie sich darauf verlassen haben, dass der Controller eines Screens zurückgesetzt wird, wenn Sie wegnavigieren, benötigen Sie jetzt autoDispose (oder, mit Code-Generierung, das ist der Standard für annotierte Provider, sofern Sie nicht ref.keepAlive() aufrufen). Prüfen Sie jeden Provider, dessen altes Verhalten von baumbasiertem Verwerfen abhing.
ref.read innerhalb von build() ist eine Falle. Das Lesen eines anderen Providers mit ref.read innerhalb eines Notifier.build() oder eines Widget-build erfasst den Wert einmal als Momentaufnahme und aktualisiert nie. Verwenden Sie ref.watch für alles, das reagieren soll, und reservieren Sie ref.read für Event-Handler wie Button-Callbacks. riverpod_lint markiert die meisten davon für Sie, weshalb es sich lohnt, die Dev-Abhängigkeit ab dem ersten Tag zu installieren.
Consumer existiert in beiden Paketen. Wenn Sie während der Migration beide importieren, ist Consumer mehrdeutig. Riverpods Consumer nimmt einen (context, ref, child)-Builder; der von provider nimmt (context, value, child). Bevorzugen Sie es, das gesamte Widget zu einem ConsumerWidget zu konvertieren, statt einen Riverpod-Consumer in ein Widget aus der provider-Ära einzustreuen, und Sie vermeiden den Import-Konflikt vollständig.
Die Migration belohnt es, langweilig zu sein: ein Blatt, ein Commit, die Tests ausführen, wiederholen. Bis Sie provider aus pubspec.yaml löschen, ist der riskante Teil bereits Wochen zuvor in kleinen, umkehrbaren Schritten passiert.
Verwandtes
- Wenn Sie von einer anderen State-Bibliothek kommen, behandelt die GetX-zu-Riverpod-Migration denselben blatt-zuerst-Ansatz für ein schwergewichtigeres Framework.
- Noch unentschlossen? Der Vergleich provider vs Riverpod vs Bloc legt die Kompromisse dar, bevor Sie sich festlegen.
- Für die asynchrone Seite geht Lade- und Fehlerzustände mit AsyncValue tief auf
whenundAsyncNotifierein. - Kommen Sie von rohem
FutureBuilder? Siehe FutureBuilder/StreamBuilder vs Riverpod AsyncValue. - Der häufigste Laufzeitfehler nach der Migration: Cannot use ref after the widget was disposed.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.