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.
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.
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();
}
}
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();
},
),
);
}
}
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.
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.
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}'));