Start Debugging

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

Was bricht

BereichÄnderungSchweregrad
Root-VerdrahtungMultiProvider ersetzt durch einen einzigen ProviderScopemittel
Readscontext.watch<T>() / Provider.of<T>(context) ersetzt durch ref.watch / ref.readhoch
NotifierChangeNotifier + notifyListeners() ersetzt durch Notifier + State-Neuzuweisunghoch
Rebuild-SemantikRiverpod vergleicht State per ==; In-Place-Mutation baut nicht mehr neu aufhoch
KompositionProxyProvider ersetzt durch ref.watch der Abhängigkeitmittel
WidgetsStatelessWidget / StatefulWidget werden zu ConsumerWidget / ConsumerStatefulWidgetmittel
Lifecycleprovider verwirft beim Entfernen aus dem Baum; Riverpod behält State bis autoDisposemittel

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

Migrationsschritte

  1. Fügen Sie Riverpod neben provider in pubspec.yaml hinzu. Entfernen Sie provider noch 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.5

    Prüfen: flutter pub get löst ohne Versionskonflikte auf.

  2. Kapseln Sie die App-Wurzel in ProviderScope und behalten Sie MultiProvider vorerst darin. ProviderScope ist der Ort, an dem Riverpod den gesamten Provider-State speichert. Es ist keine Liste von Providern, es ist eine einzelne Grenze. Lassen Sie Ihren bestehenden MultiProvider darunter, 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.

  3. Konvertieren Sie einen Blatt-ChangeNotifier in einen Notifier. Wählen Sie ein Modell, von dem nichts anderes abhängt. In provider mutieren Sie ein Feld und rufen notifyListeners() auf. In Riverpod gibt build() den initialen State zurück, und Sie weisen state neu zu, um zu benachrichtigen. Es gibt kein notifyListeners().

    // 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-outputs aus, um cartProvider zu generieren. Prüfen: Der Generator erzeugt cart_model.g.dart ohne Fehler.

  4. Stellen Sie den Screen, der ihn konsumiert, auf ein ConsumerWidget um. StatelessWidget wird zu ConsumerWidget, und build erhält ein WidgetRef ref. context.watch<CartModel>() wird zu ref.watch(cartProvider). Für einen Methodenaufruf wird context.read<CartModel>().add(x) zu ref.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 ConsumerStatefulWidget plus ConsumerState, wobei ref als Feld verfügbar ist. Entfernen Sie die Zeile ChangeNotifierProvider(create: (_) => CartModel()) aus MultiProvider. Prüfen: Der Screen verhält sich gleich, und die MultiProvider-Liste ist um eins kürzer.

  5. Ersetzen Sie ProxyProvider durch ref.watch-Komposition. Das ist der Schritt, der den meisten Code löscht. Ein ProxyProvider, 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ür providers context.select, und es bedeutet, dass apiClient nur dann neu aufgebaut wird, wenn sich token ändert, nicht bei jedem AuthModel-Update. Prüfen: Abhängige Widgets bauen neu auf, wenn sich der vorgelagerte Provider ändert.

  6. Migrieren Sie FutureProvider und StreamProvider zu ihren Riverpod-Äquivalenten. Die Namen sind gleich; nur die Verdrahtung unterscheidet sich. Ein provider-FutureProvider wird mit context.watch<AsyncSnapshot>-artiger Verdrahtung gelesen; der Riverpod-Provider gibt ein AsyncValue<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.

  7. Löschen Sie die provider-Abhängigkeit. Sobald MultiProvider leer ist, entfernen Sie es aus main.dart, lassen dann provider: ^6.1.5 aus pubspec.yaml fallen und führen flutter pub get aus. Der Compiler wird alle verbleibenden context.watch/context.read/Provider.of-Aufrufe markieren. Prüfen: Das Projekt kompiliert mit null Referenzen auf package:provider.

Verifizierung

Führen Sie diese Checkliste nach dem letzten Schritt aus, nicht nur ganz am Ende:

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

Quellen

Comments

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

< Zurück