Da `ChangeNotifier`
All'interno di Riverpod, ChangeNotifierProvider
deve essere utilizzato per offrire una transizione
graduale da pkg:provider.
Se hai appena iniziato una migrazione verso Riverpod, assicurati di leggere la guida dedicata
(consulta Quickstart).
Questo articolo è pensata per chi è già transizionato su Riverpod ma vuole distaccarsi dall'usare
ChangeNotifier
in modo definitivo.
Nel complesso, la migrazione da ChangeNotifier
a AsyncNotifer
richiede un cambio di paradigma,
ma comporta una grande semplificazione con il codice migrato risultante.
Consulta anche Why Immutability.
Prendiamo in considerazione questo esempio (difettoso):
class MyChangeNotifier extends ChangeNotifier {
MyChangeNotifier() {
_init();
}
List<Todo> todos = [];
bool isLoading = true;
bool hasError = false;
Future<void> _init() async {
try {
final json = await http.get('api/todos');
todos = [...json.map(Todo.fromJson)];
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> addTodo(int id) async {
isLoading = true;
notifyListeners();
try {
final json = await http.post('api/todos');
todos = [...json.map(Todo.fromJson)];
hasError = false;
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}
}
final myChangeProvider = ChangeNotifierProvider<MyChangeNotifier>((ref) {
return MyChangeNotifier();
});
Questa implementazione mostra diverse lacune di progettazione come:
- L'utilizzo di
isLoading
ehasError
per gestire differenti casi asincroni - La necessita di gestire attentamente le richieste attraverso le espressioni
try
/catch
/finally
- La necessita di invocare
notifyListeners
nei momenti giusti per far sì che questa implementazione funzioni - La presenza di stato incoerenti o possibilmente indesiderabile, ad esempio l'inizializzazione con una lista vuota
Si noti come questo esempio è stato realizzato per mostrare come ChangeNotifier
possa portare a scelte
di design difettose per sviluppatori alle prime armi; inoltre, un'altra conclusione è che lo stato mutabile
potrebbe essere molto più difficile di quanto promesso inizialmente.
Notifier
/AsyncNotifer
, in combinazione con lo stato immutabile, può portare a scelte di design migliori
e meno errori.
Vediamo come migrare lo snippet precedente, un passo alla volta, verso le API più recenti
Inizia la tua migrazione
Per prima cosa, dovremmo dichiarare il nuovo provider / notifier: questo richiede ci richiede una riflessione su cosa dobbiamo fare che dipende dalla tua unica business logic.
Riassumiamo le richieste:
- Lo stato è rappresentato con una
List<Todo>
, che è ottenuta da una chiamata di rete, con nessun parametro - Lo stato dovrebbe anche esporre informazioni sui suoi stati di
loading
,error
edata
- Lo stato può essere mutato attraverso dei metodi esposti, quindi una funzione non è sufficiente
La riflessione appena fatta si riduce alla risposta alle seguenti domande:
- Sono richiesti dei side effects?
y
: Utilizza le API di Riverpod basate su classin
: Utilizza le API di Riverpod basate sulle funzioni
- Lo stato necessita di essere caricato in modo asincrono?
y
: Permettiamo abuild
di restituireFuture<T>
n
: Permettiamo abuild
di restituire semplicementeT
- Sono richiesti dei parametri?
y
: Permettiamo abuild
(o alla tua funzione) di accettarlin
: Non permettiamo abuild
(o alla tua funzione) di accettare parametri extra
Se stai utilizzando la generazione di codice, la riflessione precedente è abbastanza.
Non è necessario pensare ai nomi corretti delle classi e alle loro API specifiche.
@riverpod
ti chiede solamente di scrivere una classe con il suo tipo da restituire, e questo basta.
Tecnicamente, la mossa migliore qui è definire un AutoDisposeAsyncNotifier<List<Todo>>
,
che coprirà tutti i requisiti richiesti. Scriviamo per prima del pseudocodice.
class MyNotifier extends _$MyNotifier {
FutureOr<List<Todo>> build() {
// TODO ...
return [];
}
Future<void> addTodo(Todo todo) async {
// TODO
}
}
Ricorda: utilizza gli snippet nel tuo IDE per avere una sorta di guida, oppure per velocizzare la scrittura del codice. Consulta Introduzione.
Rispetto all'implementazione di ChangeNotifier
, non abbiamo più bisogno di dichiarare todos
;
tale variabile è contenuta in state
, che è implicitamente caricato con build
.
Infatti, i notifier di Riverpod possono esporre una entità alla volta.
Le API di Riverpod sono pensate per essere granulari, tuttavia, durante la migrazione, puoi comunque definire un'entità personalizzata per contenere più valori. Inizialmente, valuta l'utilizzo dei record di Dart 3 per semplificare la migrazione.
Inizializzazione
Inizializzare un notifier è facile: basta scrivere la logica di inizializzazione dentro il metodo build
.
Possiamo ora sbarazzarci della vecchia funzione _init
.
class MyNotifier extends _$MyNotifier {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
}
Rispetto alla vecchia funzone _init
, al nuovo metodo build
non manca nulla: non c'è più bisogno di
inizializzare variabili come isLoading
o hasError
.
Riverpod tradurrà automaticamente qualsiasi provider asincrono esponendo un AsyncValue<List<Todo>>
e
gestirà le complessità dello stato asincrono in modo decisamente migliore rispetto a quello che possono fare
due semplici variabili booleane.
Infatti, qualsiasi AsyncNotifier
rende effettivamente la scrittura aggiuntiva di try
/catch
/finally
un anti-pattern per la gestione dello stato asincrono.
Mutazioni e Side effects
Proprio come l'inizializzazione, quando si eseguono dei side effects non c'è la necessità di manipolare
variabili booleani come hasError
, o di scrivere blocchi try
/catch
/finally
aggiuntivi.
Di seguito, abbiamo tagliato tutto il codice boilerplate e migrato con successo l'esempio di sopra:
class MyNotifier extends _$MyNotifier {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
Future<void> addTodo(Todo todo) async {
// optional: state = const AsyncLoading();
final json = await http.post('api/todos');
final newTodos = [...json.map(Todo.fromJson)];
state = AsyncData(newTodos);
}
}
La sintassi e le scelte di progettazione possono variare, ma alla fine, quello di cui abbiamo bisogno è scrivere le nostre richieste ed aggiornare lo stato in seguito. Consulta Eseguire side effects.
Riassunto del processo di migrazione
Rivediamo l'intero processo di migrazione applicato in questa pagina, da un punto di vista operazionale.
- Abbiamo spostato l'inizializzazione, da un metodo personalizzato invocato in un costruttore, al metodo
build
- Abbiamo rimosso le proprietà
todos
,isLoading
ehasError
: la proprietà internastate
sarà sufficiente - Abbiamo rimosso qualsiasi tipo blocco
try
-catch
-finally
, restituire il future è sufficiente - Abbiamo applicato la stessa simplificazione sui side effects (
addTodo
) - Abbiamo applicato le mutazioni, semplicemente riassegnando
state