Zum Hauptinhalt springen

Reading a provider

Bevor sie diesen Leitfaden lesen, sollten sie zuerst über Provider lesen.

In diesem Leitfaden wird gezeigt, wie man einen Provider konsumiert/verarbeitet.

Abrufen eines "ref" Objekts

Bevor wir einen Provider lesen können, müssen wir zunächst ein "ref"-Objekt erhalten.

Dieses Objekt ermöglicht es uns, mit Providern zu interagieren, sei es von einem Widget oder einem anderen Provider.

Abrufen eines "ref" von einem Provider

Alle Provider erhalten ein "ref" als Parameter:

final provider = Provider((ref) {
// ref verwenden, um andere Anbieter zu finden
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

Dieser Parameter kann sicher an den vom Provider angegebenen Wert übergeben werden.

Ein üblicher Anwendungsfall ist zum Beispiel die Übergabe des "ref" des Providers an einen StateNotifier:


final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref) : super(0);

final Ref ref;

void increment() {
// Counter kann "ref" dazu benutzen um andere Provider auszulesen
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

Auf diese Weise könnte unsere Klasse counter-Provider auslesen.

Abrufen eines "ref" von einem Widget

Widgets verfügen natürlich nicht über einen ref-Parameter. Aber Riverpod bietet mehrere Lösungen, um einen von Widgets zu erhalten.

Erweiterung des ConsumerWidget anstelle des StatelessWidget

Die häufigste Lösung besteht darin, StatelessWidget durch ConsumerWidget zu ersetzen, wenn man ein Widget erstellt.

Das ConsumerWidget ist im Grunde identisch mit dem StatelessWidget, mit dem einzigen Unterschied ist, dass es einen zusätzlichen Parameter in seiner Build-Methode hat: das "ref"-Objekt.

Ein typisches ConsumerWidget würde wie folgt aussehen:


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


Widget build(BuildContext context, WidgetRef ref) {
// Benutze ref um auf Provider zu lauschen
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Erweitern von ConsumerStatefulWidget+ConsumerState anstelle von StatefulWidget+State

Ähnlich wie ConsumerWidget sind ConsumerStatefulWidget und ConsumerState das Äquivalent zu einem StatefulWidget mit seinem State, mit dem Unterschied, dass der State ein "ref"-Objekt hat.

Dieses Mal wird das "ref" nicht als Parameter der Build-Methode übergeben, sondern ist eine Eigenschaft des ConsumerState-Objekts:


class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" kann in allen Lebenszyklen eines StatefulWidget verwendet werden.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// Wir können auch "ref" verwenden, um einen Provider innerhalb der Build-Methode abzuhören
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Erweiterung des HookConsumerWidget anstelle des HookWidget

Diese Lösung ist spezifisch für flutter_hooks-Benutzer. Da flutter_hooks die Erweiterung von HookWidget erfordert, um zu funktionieren, können Widgets, die Hooks verwenden, ConsumerWidget nicht erweitern.

Eine Lösung ist, durch das Paket hooks_riverpod, HookWidget durch HookConsumerWidget zu ersetzen. HookConsumerWidget agiert sowohl als ein ConsumerWidget als auch als ein HookWidget. Dies ermöglicht es einem Widget, sowohl auf Provider zu hören als auch Hooks zu verwenden.

Ein Beispiel wäre:


class HomeView extends HookConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget erlaubt es Hooks innerhalb der Build Methode zu benutzen
final state = useState(0);

// Wir können auch den ref Parameter dazu benutzen um auf einen Provider zu lauschen
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Consumer und HookConsumer Widgets

Eine letzte Lösung, um ein "ref" innerhalb von Widgets zu erhalten, ist die Verwendung von Consumer/HookConsumer.

Diese Klassen sind Widgets, die verwendet werden können, um ein "ref" zu erhalten, mit den gleichen Eigenschaften wie ConsumerWidget/HookConsumerWidget.

Diese Widgets können eine Möglichkeit sein, ein "ref" zu erhalten, ohne eine Klasse definieren zu müssen. Ein Beispiel wäre:

Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// Wie HookConsumerWidget können wir Hooks innerhalb des Builders verwenden
final state = useState(0);

// Wir können auch den ref-Parameter verwenden, um auf Provider zulauschen.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);

Verwendung von ref zur Interaktion mit Provider

Jetzt, da wir einen "ref" haben, können wir es verwenden.

Es gibt drei Hauptverwendungszwecke für "ref":

  • den Wert eines Providers zu erhalten und auf Änderungen zu achten, so dass, wenn sich dieser Wert ändert, das Widget oder der Provider, der den Wert abonniert hat, neu aufgebaut wird. Das kann mit ref.watch gemacht werden
  • Hinzufügen eines Listeners für einen Provider, um eine Aktion auszuführen, wenn sich dieser Provider ändert. Das kann mit ref.listen gemacht werden.
  • den Wert eines Providers zu erhalten, während Änderungen ignoriert werden. Dies ist nützlich, wenn wir den Wert eines Providers in einem Ereignis wie "on click" benötigen. Dies kann mit ref.read geschehen.
note

Wann immer es möglich ist, verwenden Sie lieber ref.watch als ref.read oder ref.listen, um eine Funktion zu implementieren.
Wenn Sie Ihre Implementierung so ändern, dass sie sich auf ref.watch verlässt, wird sie sowohl reaktiv als auch deklarativ, was Ihre Anwendung wartungsfreundlicher macht.

Verwendung von ref.watch um einen Provider zu beobachten

Es ist möglich, ref.watch innerhalb der build Methode eines Widgets oder innerhalb des Körpers eines Providers zu verwenden, damit das Widget/ der Provider auf den Provider hört:

Zum Beispiel könnte ein Provider ref.watch verwenden, um mehrere Provider zu einem neuen Wert zu kombinieren.

Ein Beispiel wäre das Filtern einer ToDo-Liste. Wir könnten zwei Provider haben:

  • filterTypeProvider, ein Provider, der den aktuellen Filtertyp angibt (keiner, nur erledigte Aufgaben anzeigen, ...)
  • todosProvider, ein Provider, der die gesamte Liste der Aufgaben anzeigt

Damit könnten wir unter Verwendung von ref.watch einen dritten Provider erstellen, der beide Provider kombiniert, um eine gefilterte Liste von Aufgaben zu erstellen:


final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider =
StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
// erhält sowohl den Filter als auch die Liste der Todos
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// Rückgabe der vollständigen Liste der Todos
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// Rückgabe der ungefilterten Liste der Todos
return todos;
}
});

Mit diesem Code zeigt filteredTodoListProvider nun die gefilterte Liste der Aufgaben an.

Die gefilterte Liste wird auch automatisch aktualisiert, wenn der Filter oder die Liste der Aufgaben geändert wird. Gleichzeitig wird die gefilterte Liste aber nicht neu berechnet, wenn weder der Filter noch die Liste der Aufgaben geändert wurde.

Ähnlich könnte ein Widget ref.watch verwenden, um eine Benutzeroberfläche anzuzeigen, die von einem von einem Provider abhängt:


final counterProvider = StateProvider((ref) => 0);

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


Widget build(BuildContext context, WidgetRef ref) {
// benutze ref um auf einen Provider zu lauschen
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

In diesem Ausschnitt sehen Sie ein Widget, das auf einen Provider zugreift, der einen Zähler speichert. Wenn sich dieser Zähler ändert, wird das Widget neu aufgebaut und das UI wird aktualisiert, um den neuen Wert anzuzeigen.

caution

Die Methode watch sollte nicht asynchron aufgerufen werden, wie innerhalb von onPressed oder einem ElevatedButton. Auch sollte sie nicht innerhalb von initState und anderen State Lebenszyklen verwendet werden.

In diesen Fällen sollte man stattdessen ref.read verwenden.

Verwendung von ref.listen zur Reaktion auf einen Providerwechsel

Ähnlich wie bei ref.watch ist es möglich, ref.listen zu verwenden, um einen Provider zu beobachten.

Der Hauptunterschied zwischen den beiden besteht darin, dass das Widget/ der Provider nicht neu aufgebaut wird, wenn sich der beobachtete Provider ändert, sondern dass mit ref.listen eine eigene Funktion aufgerufen wird.

Das kann nützlich sein, um Aktionen auszuführen, wenn eine bestimmte Änderung eintritt, z.B. um eine Snackbar anzuzeigen, wenn ein Fehler auftritt.

Die Methode ref.listen benötigt 2 Positionsargumente, das erste ist der Provider und das zweite ist die Callback-Funktion, die wir ausführen wollen, wenn sich der Zustand ändert. Wenn die Callback-Funktion aufgerufen wird, gibt sie zwei Werte zurück: den Wert des vorherigen Zustands und den Wert des neuen Zustands.

Die Methode ref.listen kann innerhalb des Body eines Providers verwendet werden:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});

oder innerhalb der build Method eines Widgets:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

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


Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});

return Container();
}
}
caution

Die Methode listen sollte nicht asynchron aufgerufen werden, wie innerhalb von onPressed oder einem ElevatedButton. Sie sollte auch nicht innerhalb von initState und anderen State-Lebenszyklen verwendet werden.

Verwendung von ref.read, um den Status eines Providers einmalig zu erhalten

Die Methode ref.read ist eine Möglichkeit, den Status eines Providers zu erhalten, ohne dass dies einen zusätzlichen Effekt hat.

Sie wird üblicherweise innerhalb von Funktionen verwendet, das durch UI ausgelöst werden. Zum Beispiel können wir ref.read verwenden, um einen Zähler zu erhöhen, wenn ein Benutzer auf eine Schaltfläche klickt:


final counterProvider =
StateNotifierProvider<Counter, int>(Counter.new);

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


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Aufruf von `increment()` der `Counter` Klasse
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
note

Die Verwendung von ref.read sollte so weit wie möglich vermieden werden.

Es existiert als Ausweichmöglichkeit für Fälle, in denen die Verwendung von watch oder listen sonst zu unbequem wäre.
Wenn Sie die Möglichkeit haben, ist es fast immer besser, watch/listen zu verwenden, insbesondere watch.

Verwenden sie ref.read nicht innerhalb der Build-Methode

Man könnte versucht sein, ref.read zu verwenden, um die Leistung eines Widgets zu optimieren, indem man folgendes tut:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
// Benutze "read" um die Updates eines Providers zu ignorieren
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Dies ist jedoch eine sehr schlechte Praxis und kann zu Fehlern führen, die schwer zu finden sind.

Die Verwendung von ref.read auf diese Weise wird häufig mit dem Gedanken in Verbindung gebracht, dass sich der von einem Provider bereitgestellte Wert niemals ändert, so dass die Verwendung von ref.read sicher ist.
Das Problem mit dieser Annahme ist, dass der Provider heute zwar seinen Wert vielleicht nie aktualisiert, aber es gibt keine Garantie dafür, dass das auch morgen noch so sein wird.

Software neigt dazu, sich häufig zu ändern, und es ist wahrscheinlich, dass ein Wert, der sich bisher nie geändert hat, in Zukunft geändert werden muss.
Wenn Sie aber ref.read verwenden würden, müssten Sie, wenn sich dieser Wert ändert, Ihren gesamten Code durchgehen, um ref.read in ref.watch zu ändern - was fehleranfällig ist und Sie werden wahrscheinlich einige Fälle vergessen.

Wenn Sie hingegen von Anfang an ref.watch verwenden würden, hätten Sie kein Problem.

Aber ich wollte ref.read verwenden, um die Anzahl der Neuaufbauten meines Widgets zu reduzieren

Obwohl das Ziel lobenswert ist, ist es wichtig zu beachten, dass man genau den gleichen Effekt (die Reduzierung der Anzahl der Builds) erreichen kann, wenn man stattdessen ref.watch verwendet.

Provider bieten verschiedene Möglichkeiten, einen Wert zu erhalten und gleichzeitig die Anzahl der Rebuilds zu reduzieren, die Sie stattdessen verwenden könnten.

Zum Beispiel anstelle von


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Man könnte folgendes tun:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Beide Schnipsel haben den gleichen Effekt: Unsere Schaltfläche wird nicht neu aufgebaut, wenn der Zähler erhöht wird.

Auf der anderen Seite unterstützt der zweite Ansatz Fälle, in denen der Zähler zurückgesetzt wird.
Zum Beispiel könnte ein anderer Teil der Anwendung Folgendes aufrufen:

ref.refresh(counterProvider);

was das StateController-Objekt neu erstellen würde.

Wenn wir hier ref.read verwenden würden, würde unsere Schaltfläche immer noch die vorherige StateController-Instanz verwenden, die entsorgt wurde und nicht mehr verwendet werden sollte.
Bei Verwendung von ref.watch hingegen wird die Schaltfläche korrekt neu erstellt, um den neuen StateController zu verwenden.

Entscheiden, was zu lesen ist

Je nach Provider, den Sie abhören wollen, können Sie mehrere mögliche Werte haben, die Sie abhören können.

Nehmen wir als Beispiel den folgenden StreamProvider:

final userProvider = StreamProvider<User>(...);

Wenn Sie diesen userProvider lesen, können Sie:

  • den aktuellen Status synchron lesen, indem man auf den userProvider selbst hört:

    Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<User> user = ref.watch(userProvider);

    return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
    );
    }
  • den zugehörigen Stream durch Abhören von "userProvider.stream" erhalten:

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • einen Future erhalten, der mit dem zuletzt ausgegebenen Wert aufgelöst wird, indem man auf userProvider.future hört:

    Widget build(BuildContext context, WidgetRef ref) {
    Future<User> user = ref.watch(userProvider.future);
    }

Andere Provider können andere Alternativwerte anbieten.
Weitere Informationen finden Sie in der Dokumentation des jeweiligen Providers, indem Sie die API reference besuchen.

Verwendung von "select" um Rebuilds zu filtern

Eine letzte Funktion, die im Zusammenhang mit dem Lesen von Providern zu erwähnen ist, ist die Möglichkeit, die Anzahl der Neuaufbauten eines Widgets/Providers bzw. die Häufigkeit der Ausführung einer Funktion durch ref.listen zu reduzieren.

Dies ist wichtig zu beachten, da das Abhören eines Providers standardmäßig das gesamte Objekt abhört. Aber in einigen Fällen kann es sein, dass ein Widget/Provider sich nur für einige Eigenschaften interessiert und nicht für das gesamte Objekt.

Zum Beispiel kann ein Provider einen User ausstellen:

abstract class User {
String get name;
int get age;
}

Ein Widget darf jedoch nur den Benutzernamen verwenden:

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

Wenn wir naiv ref.watch verwenden würden, würde dies das Widget neu aufbauen, wenn sich das Alter des Benutzers ändert.

Die Lösung ist die Verwendung von select, um Riverpod explizit mitzuteilen, dass wir nur auf einige Eigenschaften des User hören wollen.

Der aktualisierte Code würde lauten:

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}

Die Funktionsweise besteht darin, dass wir durch die Verwendung von select eine Funktion angeben, die die gewünschte Eigenschaft zurückgibt.

Wenn sich der User ändert, ruft Riverpod diese Funktion auf und vergleicht das vorherige und das neue Ergebnis. Wenn sie unterschiedlich sind (z. B. wenn sich der Name geändert hat), erstellt Riverpod das Widget neu. Wenn sie jedoch gleich sind (z. B. wenn sich das Alter geändert hat), wird Riverpod das Widget nicht das Widget nicht neu erstellt.

info

Es ist auch möglich, select mit ref.listen zu verwenden:

ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

Auf diese Weise wird der Listener nur aufgerufen, wenn sich der Name ändert.

tip

Sie müssen nicht unbedingt eine Eigenschaft des Objekts zurückgeben. Jeder Wert, der == überschreibt, funktioniert. Sie könnten zum Beispiel so vorgehen:

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));