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:
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.