Zum Hauptinhalt springen

StateProvider

StateProvider ist ein Provider, der eine Möglichkeit zur Änderung seines Zustands anbietet. Es ist eine vereinfachte Version von [StateNotifierProvier], entworfen um das Schreiben einer StateNotifier-Klasse bei sehr einfachen Anwendungsfällen zu vermeiden.

StateProvider existiert haupstächlich um einfache Variablen über das UI zu modifizieren. Der Zustand eines StateProvider ist typischerweise:

  • ein Enum, z.B. ein Filtertyp
  • ein String, üblicherweise der Inhalt eines Textfields
  • ein boolean für Checkboxes
  • eine number, für Seitennummerierung oder das Alter in FormField

StateProvider sollte man nicht verwenden, wenn:

  • der Zustand eine Validierungslogik benötigt
  • der Zustand ein komplexes Objekt ist (wie z.B. eine eigene Klasse oder List/Map, ...)
  • die Modifizierungslogik komplizierter ist und über ein einfaches count++ hinausgeht.

Für fortgeschrittene Fälle sollte stattdessen ein StateNotifierProvider verwende und eine StateNotifier-Klasse erstellt werden. Trotz des etwas längeren, initialen Boilplate Codes, ist die Implementierung einer eigenen StateNotifier-Klasse entscheidend für die langfristige Wartbarkeit des Projektes, da die Geschäftslogik und der Zustand an einem zentralen Ort vorliegen.

Anwendungsbeispiel: Filtertyp per Dropdown ändern

Ein praktischer Anwendungsfall für einen StateProvider wäre die Verwaltung des Zustands von einfachen Formularkomponenten wie Dropdowns/Textfelder/Checkboxen. Insbesondere werden wir sehen, wie man einen StateProvider verwendet, um ein Dropdown zu implementieren, das es erlaubt, die Sortierung einer Liste von Produkten zu ändern.

Der Einfachheit halber wird die Liste der Produkte, die wir erhalten werden direkt in der Anwendung erstellt und sieht wie folgt aus:


class Product {
Product({required this.name, required this.price});

final String name;
final double price;
}

final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];

final productsProvider = Provider<List<Product>>((ref) {
return _products;
});

In einer realen Anwendung würde diese Liste typischerweise mit Hilfe von FutureProvider durch eine Netzwerkanfrage abgerufen werden.

Das UI könnte dann die Liste der Produkte anzeigen, indem sie:


Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}

Nun, da wir mit der Basis fertig sind, können wir eine Auswahlliste hinzufügen, mit der wir unsere Produkte entweder nach Preis oder nach Name filtern können.
Dafür werden wir DropDownButton benutzen.


// An enum representing the filter type
enum ProductSortType {
name,
price,
}

Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ... /* SKIP */
itemBuilder: (c, i) => Container(), /* SKIP END */
),
);
}

Nun, da wir ein Dropdown haben, erstellen wir einen StateProvider und synchronisieren den Status des Dropdowns mit unserem Provider.

Zuerst erstellen wir einen StateProvider:


final productSortTypeProvider = StateProvider<ProductSortType>(
// We return the default sort type, here name.
(ref) => ProductSortType.name,
);

Dann können wir diesen Provider mit unserem Dropdown verbinden:

DropdownButton<ProductSortType>(
// When the sort type changes, this will rebuild the dropdown
// to update the icon shown.
value: ref.watch(productSortTypeProvider),
// When the user interacts with the dropdown, we update the provider state.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),

Damit sollten wir nun in der Lage sein, die Sortierart zu ändern. Auf die Liste der Produkte hat dies allerdings noch keine Auswirkungen! Es ist nun Zeit für den letzte Teil: Wir aktualisieren unseren productsProvider, um die Liste der Produkte zu sortieren.

Eine Schlüsselkomponente der Implementierung ist die Verwendung von ref.watch, damit unser productsProvider die Sortierart erhält und die Liste der Produkte neu berechnet, wenn sich die Sortierart ändert.

Die Implementierung würde wie folgt aussehen:


final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});

Das ist alles! Diese Änderung reicht aus, damit die Benutzeroberfläche die Liste der Produkte automatisch neu anzeigt, wenn sich der Sortierart ändert.

Hier ist das vollständige Beispiel auf Dartpad:

Wie kann der Status auf der Grundlage des vorherigen Wertes aktualisiert werden, ohne den Provider zweimal zu lesen?

Manchmal möchte man den Zustand eines StateProvider auf der Grundlage des vorherigen Wertes aktualisieren. Natürlich kann es passieren, dass man am Ende schreibt:


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

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


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// We're updating the state from the previous value, we ended-up reading
// the provider twice!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}

Dieser Ausschnitt ist zwar nicht besonders schlecht, aber die Syntax ist etwas umständlich.

Um die Syntax etwas zu verbessern, können wir die Funktion update verwenden. Diese Funktion nimmt einen Callback entgegen, der den aktuellen Zustand empfängt und den neuen Zustand zurückgeben soll.
Wir können sie verwenden, um unseren vorherigen Code zu refectorn:


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

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


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}

Mit dieser Änderung wird die gleiche Wirkung erzielt, wobei die Syntax erheblich verbessert wird.