Zum Hauptinhalt springen
⚠️The documentation for version 2.0 is in progress. A preview is available at: https://docs-v2.riverpod.dev

Provider

Provider ist der einfachste aller Provider. Er erzeugt einen Wert... Und das war's auch schon.

Provider wird überlicherweise für folgendes eingesetzt:

  • Zwischenspeicherung von Berechnungen
  • einen Wert anderer Provider (wie z.B. Repository/HttpClient) zur Verfügung stellen.
  • eine Möglichkeit für Tests oder Widgets, einen Wert außer Kraft zu setzen.
  • Verringerung des Neuaufbaus von Provider/Widgets, ohne select verwenden zu müssen.

Verwendung von Provider zur Zwischenspeicherung von Berechnungen

Provider ist ein leistungsfähiges Werkzeug für die Zwischenspeicherung synchroner Operationen, wenn es mit ref.watch kombiniert wird.

Ein Beispiel wäre das Filtern einer Liste von ToDos.
Da das Filtern einer Liste etwas kostspielig sein könnte, wollen wir idealerweise die Liste der ToDo's nicht jedes Mal herausfiltern, wenn sich unsere Anwendung neu aufbaut.
In dieser Situation könnten wir Provider verwenden, um den Filter für uns zu erledigen.

Dazu nehmen wir an, dass unsere Anwendung einen bestehenden StateNotifierProvider hat, der eine Liste von Todos bearbeitet:

class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}

class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);

void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO add other methods, such as "removeTodo", ...
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});

Von dort aus können wir Provider verwenden, um die gefilterte Liste der ToDo's zu veröffentlichen, die nur nur die erledigten Aufgaben:

final completedTodosProvider = Provider<List<Todo>>((ref) {
// We obtain the list of all todos from the todosProvider
final todos = ref.watch(todosProvider);

// we return only the completed todos
return todos.where((todo) => todo.isCompleted).toList();
});

Indem wir auf completedTodosProvider lauschen, ist die UI nun in der Lage, die Liste der erledigten ToDos anzuzeigen:

Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO show the todos using a ListView/GridView/...
});

Das Interessante ist, dass die Listenfilterung jetzt zwischengespeichert wird.

Das bedeutet, dass die Liste der erledigten ToDos nicht neu berechnet wird, bis ToDos hinzugefügt/entfernt/aktualisiert werden, selbst wenn wir die Liste der erledigten ToDos mehrmals lesen.

Beachten Sie, dass wir den Cache nicht mehr manuell ungültig machen müssen, wenn sich die Liste der ToDos ändert. Der Provider ist automatisch in der Lage zu wissen, wann das Ergebnis neu berechnet werden muss dank ref.watch.

Verringerung von Provider/Widget Neuerstellungen durch Verwendung Provider

Ein einzigartiger Aspekt von Provider ist, dass selbst wenn Provider neu berechnet wird (typischerweise bei der Verwendung von ref.watch), werden die Widgets/Provider, die darauf lauschen, nicht aktualisiert, es sei denn, der Wert hat sich geändert.

Ein Beispiel aus der realen Welt wäre das Aktivieren/Deaktivieren der vorherigen/nächsten Schaltflächen in einer paginierten Ansicht:

stepper example

In unserem Fall werden wir uns speziell auf die Schaltfläche "Zurück" konzentrieren.
Eine naive Implementierung einer solchen Schaltfläche wäre ein Widget, das den aktuellen Seitenindex ermittelt, und wenn dieser Index gleich 0 ist, würden wir die Schaltfläche deaktivieren.

Der Code könnte so aussehen:

final pageIndexProvider = StateProvider<int>((ref) => 0);

class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}): super(key: key);


Widget build(BuildContext context, WidgetRef ref) {
// if not on first page, the previous button is active
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}

Das Problem bei diesem Code ist, dass die Schaltfläche "Zurück" jedes Mal neu erstellt wird, wenn wir die aktuelle Seite wechseln. In der idealen Welt würden wir wollen, dass die Schaltfläche nur beim Wechsel zwischen aktiviert und deaktiviert neu aufgebaut wird.

Das Problem besteht darin, dass wir berechnen, ob der Benutzer direkt über die Schaltfläche "Zurück" zur vorherigen Seite wechseln darf.

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, diese Logik außerhalb des Widgets und in einen Provider auszulagern:

final pageIndexProvider = StateProvider<int>((ref) => 0);

// A provider which computes whether the user is allowed to go to the previous page
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) != 0;
});

class PreviousButton extends ConsumerWidget {
const PreviousButton({Key? key}): super(key: key);


Widget build(BuildContext context, WidgetRef ref) {
// We are now watching our new Provider
// Our widget is no longer calculating whether we can go to the previous page.
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}

Durch dieses kleine Refactoring wird unser PreviousButton Widget nicht mehr neu aufgebaut, wenn sich der Seitenindex ändert und das dank des Provider.

Von nun an, wenn sich der Seitenindex ändert, wird unser canGoToPreviousPageProvider Provider neu berechnet. Aber wenn sich der Wert, der durch den Provider zur Verfügung gestellt wrid, nicht ändert, dann wird PreviousButton nicht neu erstellt.

Diese Änderung verbesserte die Leistung unserer Schaltfläche und hatte den interessanten Vorteil, die Logik außerhalb unseres Widgets zu extrahieren.