Zum Hauptinhalt springen

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) {
// Wir erhalten die Liste aller Todos vom todosProvider
final todos = ref.watch(todosProvider);

// wir geben nur die erledigten ToDos zurück
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/.../* SKIP */
return Container();
/* SKIP END */
});

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({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// Wenn nicht auf der ersten Seite, dann ist der previous Knopf aktiv
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);

// Ein Provider, der berechnet, ob der Benutzer zur vorherigen Seite wechseln darf
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) == 0;
});

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


Widget build(BuildContext context, WidgetRef ref) {
// Wir beobachten jetzt unseren neuen Provider.
// Unser Widget berechnet nicht mehr, ob wir zur vorherigen Seite wechseln können.
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.