Da `StateNotifier`
Con Riverpod 2.0, nuovi classi
sono state introdotte: Notifier
/ AsyncNotifer
.
StateNotifier
è ora sconsigliato a favore di queste nuove API.
Questa pagina mostra come migrare dal deprecato StateNotifier
a queste nuove API.
Il beneficio principale introdotto da AsyncNotifier
è un migliore supporto per operazioni async
;
infatti, AsyncNotifier
può essere pensato come un FutureProvider
che espone dei modi per essere modificato dall'UI.
Inoltre, il nuovo (Async)Notifier
:
- Espone un'oggetto
Ref
dentro la sua classe - Offre una sintassi simile tra gli approcci con o senza generazione di codice
- Offre una sintassi simile tra le sue versioni sync e async.
- Sposta la logica dai Provider e la centralizza dentro gli stessi Notifier
Vediamo come definire un Notifier
, le differenze con StateNotifier
e come migrare al nuovo
AsyncNotifier
per lo stato asincrono.
Confronto della nuova sintassi
Assicurati di sapere come definire un Notifier
prima di tuffarti in questo confronto.
Consulta Eseguire side effects.
Scriviamo un esempio, utilizzando la vecchia sintassi di StateNotifier
:
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
final counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
Di seguito lo stesso esempio, costruito con le nuove API Notifier
che si traduce approssimativamente in:
class CounterNotifier extends _$CounterNotifier {
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
Confrontando Notifier
e StateNotifier
, si possono osservare queste differenze principali:
- Le dipendenze reattive di
StateNotifier
sono dichiarate nel suo provider, mentreNotifier
centralizza questa logica nel suo metodobuild
- L'intero processo di inizializzazione di
StateNotifier
è diviso tra il suo provider e il suo costruttore, mentreNotifier
riserva un unico posto in cui collocare tale logica - Si noti come, a differenza di
StateNotifier
, nessuna logica viene mai scritta nel costruttore di unNotifier
Conclusioni simili possono essere fatte con AsyncNotifer
, ovvero l'equivalente Notifier
per operazioni asincrone.
Migrare StateNotifier
asincroni
L'aspetto più interessante della nuova sintassi API è una DX migliorata sui dati asincroni. Prendiamo il seguente esempio:
class AsyncTodosNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
AsyncTodosNotifier() : super(const AsyncLoading()) {
_postInit();
}
Future<void> _postInit() async {
state = await AsyncValue.guard(() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
});
}
// ...
}
Di seguito l'esempio precedente riscritto con le nuove API AsyncNotifier
:
class AsyncTodosNotifier extends _$AsyncTodosNotifier {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
// ...
}
AsyncNotifier
, proprio come Notifier
, introduce una più semplice ed uniforme API.
Qui è facile osservare come AsyncNotifer
sia un FutureProvider
con dei metodi.
AsyncNotifer
viene fornito con una serie di utilità e getters che StateNotifier
non possiede, come
future
e
update
.
Ciò ci permette di scrivere della logica più semplice quando gestiamo mutazioni asincrone e side-effects.
Vedere anche Eseguire side effects.
Migrare da StateNotifier<AsyncValue<T>>
a AsyncNotifer<T>
si riduce a:
- Mettere la logica di inizializzazione dentro
build
- Rimuovere qualsiasi blocco
catch
/try
in inizializzazione o nei metodi di side-effects - Rimuovere qualsiasi
AsyncValue.guard
dabuild
, poiché converteFuture
inAsyncValue
Vantaggi
Dopo questi esempi, evidenziamo ora i principali vantaggi di Notifier
e AsyncNotifier
:
- La nuova sintassi dovrebbe sembrare più semplice e più leggibile, specialmente per lo stato asincrono
- È probabile che le nuove API abbiano meno codice boilerplate in generale
- La sintassi ora è unificata, abilitando la generazione di codice; non importa la tipologia di provider che stai scrivendo (consulta Informazioni sulla generazione del codice)
Addentriamoci ed evidenziamo più differenze e somiglianze.
Modifiche esplicite a .family
e .autoDispose
Un'altra importante differenza è come sono gestite le family e la funzionalità di auto dispose con le nuove API.
Notifier
ha le sue personali controparti di .family
e .autoDispose
, come FamilyNotifier
e AutoDisposeNotifier
.
Come sempre, queste modifiche possono essere combinate (aka AutoDisposeFamilyNotifier
).
AsyncNotifer
ha le sue controparti asincrone equivalenti, (AutoDisposeFamilyAsyncNotifier
).
Questi modificatori sono esplicitamente dichiarati dentro la classe; qualsiasi parametro è iniettato direttamente
nel metodo build
in modo tale da essere disponibile alla logica di inizializzazione.
Tutto questo dovrebbe portare una maggiore leggibilità, concisione e in generale meno errori.
Prendiamo l'esempio seguente, dove un StateNotifierProvider.family
viene definito.
class BugsEncounteredNotifier extends StateNotifier<AsyncValue<int>> {
BugsEncounteredNotifier({
required this.ref,
required this.featureId,
}) : super(const AsyncData(99));
final String featureId;
final Ref ref;
Future<void> fix(int amount) async {
state = await AsyncValue.guard(() async {
final old = state.requireValue;
final result = await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount);
return max(old - result, 0);
});
}
}
final bugsEncounteredNotifierProvider =
StateNotifierProvider.family.autoDispose<BugsEncounteredNotifier, int, String>((ref, id) {
return BugsEncounteredNotifier(ref: ref, featureId: id);
});
BugsEncounteredNotifier
sembra... pesante / difficile da leggere.
Diamo un'occhiata alla sua controparte AsyncNotifier
migrata:
class BugsEncounteredNotifier extends _$BugsEncounteredNotifier {
FutureOr<int> build(String featureId) {
return 99;
}
Future<void> fix(int amount) async {
final old = await future;
final result = await ref.read(taskTrackerProvider).fix(id: this.featureId, fixed: amount);
state = AsyncData(max(old - result, 0));
}
}
La sua controparte migrata dovrebbe fornire una maggiore facilità di lettura.
I parametri .family
di (Async)Notifier
sono disponibili tramite this.arg
(oppure tramite this.paramName
quando si utilizza la generazione di codice)
I cicli di vita hanno un comportamento differente
I cicli di vita tra Notifier
/AsyncNotifier
e StateNotifier
differiscono in modo sostanziale.
Questo esempio mostra - di nuovo - come le vecchie API abbiano la logica sparsa:
class MyNotifier extends StateNotifier<int> {
MyNotifier(this.ref, this.period) : super(0) {
// 1 init logic
_timer = Timer.periodic(period, (t) => update()); // 2 side effect on init
}
final Duration period;
final Ref ref;
late final Timer _timer;
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1); // 3 mutation
if (mounted) state++; // 4 check for mounted props
}
void dispose() {
_timer.cancel(); // 5 custom dispose logic
super.dispose();
}
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
// 6 provider definition
final period = ref.watch(durationProvider); // 7 reactive dependency logic
return MyNotifier(ref, period); // 8 pipe down `ref`
});
Qui, se durationProvider
si aggiorna, MyNotifier
elimina: la sua istanza è poi re-istanziata
e il suo stato interno è quindi re-inizializzato.
Inoltre, a differenza di tutti gli altri provider, la callback dispose
deve essere definita
separatamente nella classe.
Infine, è ancora possibile scrivere ref.onDispose
nel suo provider, mostrando ancora una volta come
possa essere sparsa la logica con quest'API; potenzialmente, lo sviluppatore potrebbe dover guardare otto (8!)
posti differenti per capire il comportamento di questo Notifier!
Queste ambiguità sono risolte con Riverpod 2.0
.
Vecchio dispose
vs ref.onDispose
Il metodo dispose
di StateNotifier
si riferisce all'evento di distruzione del notificatore stesso,
ovvero è una callback che viena chiamata prima di sbarazzarsi di se stesso.
Gli (Async)Notifier
non hanno questa proprietà, dato che non vengono distrutti durante la ricostruzione;
solo il suo stato interno lo è.
Nei nuovi notifier, i cicli di vita di eliminazione sono gestiti in un solo posto, tramite ref.onDispose
(e altri), proprio come qualsiasi altro provider.
Questo semplifica l'API, e, si spera la developer experience, in modo che ci sia solo un posto a cui guardare
per comprendere gli effetti collaterali del ciclo di vita: il suo metodo build
.
In breve: per registrare una callback che si attiva prima che il suo stato interno venga ricostruito, possiamo usare
ref.onDispose
come ogni altro provider.
Puoi migrare lo snippet sopra in questo modo:
class MyNotifier extends _$MyNotifier {
int build() {
// Just read/write the code here, in one place
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1);
// `mounted` is no more!
state++; // This might throw.
}
}
In questo ultimo snippet abbiamo sicuramente introdotto della semplificazione, ma rimane ancora un problema: non
possiamo capire se i nostri notifier sono ancora attivi o no mentre eseguiamo update
.
Ciò potrebbe verificare uno StateError
indesiderato.
Non più mounted
Questo accade perché (Async)Notifier
manca della proprietà mounted
, che era disponibile con StateNotifier
.
Considerando la loro differenza nel ciclo di vita, questo ha perfettamente senso; seppur possibile, una
proprietà mounted
sarebbe fuorviante sui nuovi notifier: mounted
sarebbe quasi sempre true
.
Anche se sarebbe possibile creare una soluzione alternativa personalizzata, si consiglia di aggirare questo problema annullando l'operazione asincrona.
Annullare un'operazione può essere fatto con un Completer personalizzato, o qualsiasi derivato personalizzato.
Per esempio, se stai utilizzando Dio
per eseguire richieste di rete, considera l'utilizzo di un cancel token
(consulta anche Svuotare la cache e reagire alla cancellazione dello stato).
Pertanto, l'esempio precedente viene migrato nel seguente:
class MyNotifier extends _$MyNotifier {
int build() {
// Just read/write the code here, in one place
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
// When `cancelToken.cancel` is invoked, a custom Exception is thrown
state++;
}
}
Le API di mutazione sono le stesse di prima
Finora abbiamo mostrato le differenze tra StateNotifier
e le nuove API.
Una cosa che invece Notifier
, AsyncNotifer
e StateNotifier
condividono è il modo in cui i loro stati
possono essere consumati e mutati.
I consumer possono ottenere dati da questi tre provider con la stessa sintassi, il che è ottimo nel caso
tu stai migrando da StateNotifier
; questo si applica anche ai metodi dei notifier.
class SomeConsumer extends ConsumerWidget {
const SomeConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterNotifierProvider);
return Column(
children: [
Text("You've counted up until $counter, good job!"),
TextButton(
onPressed: ref.read(counterNotifierProvider.notifier).increment,
child: const Text('Count even more!'),
)
],
);
}
}
Altre migrazioni
Esploriamo ora le differenze meno impattanti tra StateNotifier
e Notifier
(o AsyncNotifier
)
Da .addListener
e .stream
.addListener
e .stream
di StateNotifier
possono essere utilizzati per ascoltare i cambiamenti di stato.
Queste due API sono ormai da considerarsi superate.
Ciò è intenzionale a causa del desiderio di raggiungere la completa uniformità dell'API con Notifier
, AsyncNotifier
e altri provider.
Infatti, l'utilizzo di un Notifier
o di un AsyncNotifier
non dovrebbe essere diverso da qualsiasi altro provider.
Pertanto questo:
class MyNotifier extends StateNotifier<int> {
MyNotifier() : super(0);
void add() => state++;
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
final notifier = MyNotifier();
final cleanup = notifier.addListener((state) => debugPrint('$state'));
ref.onDispose(cleanup);
// Or, equivalently:
// final listener = notifier.stream.listen((event) => debugPrint('$event'));
// ref.onDispose(listener.cancel);
return notifier;
});
Diviene questo:
class MyNotifier extends _$MyNotifier {
int build() {
listenSelf((_, next) => debugPrint('$next'));
return 0;
}
void add() => state++;
}
In poche parole: se vuoi ascoltare un Notifier
/AsyncNotifer
, usa semplicemente ref.listen
.
Consulta Combinare richieste.
Da .debugState
nei test
StateNotifier
espone .debugState
: questa proprietà è usata per gli utenti di state_notifier
per abilitare l'accesso allo stato da fuori la classe quando si è in dev mode, per scopi di testing.
Se stai utilizzando .debugState
per accedere allo stato nei test, ci sono probabilità che tu debba abbandonare
questo approccio.
Notifier
/ AsyncNotifer
non hanno una prorpietà .debugState
; invece, espongono direttamente .state
,
che è @visibleForTesting
.
EVITA di accedere a .state
dai test; se devi farlo, fallo solo e solo se hai già un
a Notifier
/ AsyncNotifer
correttamente istanziato; solo dopo, puoi accedere a .state
all'interno dei test liberamente.
Infatti, Notifier
/ AsyncNotifier
non dovrebbero essere istanziati a mano; al contrario, dovrebbero
essere interagiti utilizzando il relativo provider: non farlo romperà il notifier, a causa della mancata
inizializzazione di 'ref' e dei parametri family.
Non hai un'istanza Notifier
?
Nessun problema, puoi ottenerne una con ref.read
, proprio come leggeresti il suo stato esposto:
void main(List<String> args) {
test('my test', () {
final container = ProviderContainer();
addTearDown(container.dispose);
// Obtaining a notifier
final AutoDisposeNotifier<int> notifier = container.read(myNotifierProvider.notifier);
// Obtaining its exposed state
final int state = container.read(myNotifierProvider);
// TODO write your tests
});
}
Impara di più sui test nella loro guida dedicata. Consulta Testare i tuoi provider.
Da StateProvider
StateProvider
è stato esposto da Riverpod fin dalla sua release, ed era stato fatto per ridurre le
linee di codice per le versioni semplificate di StateNotifierProvider
.
Dato che StateNotifierProvider
è deprecato, anche StateProvider
va evitato.
Inoltre, ad oggi, non c'è nessun equivalente di StateProvider
per le nuove API.
Ciò nonostante, migrare da StateProvider
a Notifier
è semplice.
Questo:
final counterProvider = StateProvider<int>((ref) {
return 0;
});
Diventa:
class CounterNotifier extends _$CounterNotifier {
int build() => 0;
set state(int newState) => super.state = newState;
int update(int Function(int state) cb) => state = cb(state);
}
Anche se ci costa qualche LoC in più, la migrazione da StateProvider
ci consente di archiviare definitivamente StateNotifier
.