Salta al contenuto principale

Informazioni sulla generazione del codice

La generazione di codice è l'idea di utilizzare uno strumento per generare codice per noi. In Dart, ciò comporta l'inconveniente di richiedere un passaggio aggiuntivo per "compilare" un'applicazione. Anche se questo problema potrebbe essere risolto nel prossimo futuro, poiché il team Dart sta lavorando a una possibile soluzione.

Nel contesto di Riverpod, la generazione di codice riguarda cambiare leggermente la sintassi per definire un "provider". Per esempio, invece di:

final fetchUserProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
});

Usando la generazione di codice scriveremmo:


Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
}

Quando si utilizza Riverpod, la generazione di codice è completamente opzionale. È totalmente possibile utilizzare Riverpod senza. Allo stesso tempo, Riverpod abbraccia la generazione di codice e consiglia di utilizzarla.

Per informazioni su come installare ed usare il generatore di codice di Riverpod, fare riferimento alla pagina Introduzione. Assicurati di abilitare l'opzione per la generazione di codice nella barra laterale della documentazione.

Dovrei usare la generazione di codice?

La generazione di codice è opzionale in Riverpod. Con questo in mente, ti starai chiedendo se dovresti usarla o no.

La risposta è: Molto probabilmente Sì. Usare la generazione di codice è la modalità consigliata per utilizzare Riverpod. È l'approccio più a prova di futuro e ti permetterà di usare Riverpod al suo massimo potenziale. Allo stesso tempo, molte applicazioni includono già la generazione di codice con pacchetti come as Freezed o json_serializable. In questi casi, il tuo progetto è già probabilmente pronto per la generazione di codice con Riverpod.

Attualmente, la generazione di codice è opzionale perché build_runner non piace a molti. Ma una volta che la Metaprogrammazione Statica sarà disponibile in Dart, build_runner non sarà più un problema. A quel punto, la sintassi della generazione di codice sarà l'unica disponibile in Riverpod.

Se usare build_runner è bloccante per te, allora e solo allora dovresti considerare di non utilizzare la generazione di codice. Ma tieni a mente che non avrai a disposizione alcune feature, e che comunque dovrai migrare alla generazione di codice in futuro. Tuttavia, quando ciò accadrà, Riverpod fornirà uno strumento di migrazione per rendere la transizione nel modo più agevole possibile.

Quali sono i benefici dell'utilizzare la generazione di codice?

Ti starai chiedendo: "Se la generazione di codice è opzionale in Riverpod, perché usarla?"

Stesso discorso per i pacchetti: Per rendere la tua vita più facile. Questo include ma non si limita a:

  • una sintassi migliore, più leggibile/flessibile e con una curva di apprendimento minore.
    • Non c'è bisogno di preoccuparsi di scegliere la tipologia di provider. Scrivi la tua logica e Riverpod sceglierà il provider più adatto all'esigenza.
    • La sintassi non dà più l'idea che stiamo definendo una "sporca variabile globale". Stiamo invece definendo una funzione/classe personalizzata.
    • Passare parametri ai provider è ora possibile senza restrizioni. Invece di essere limitati ad usare .family e passare un singolo parametro posizionale, puoi invece ora passare qualsiasi parametro. Ciò include parametri denominati, opzionali e anche valori di default.
  • hot-reload con stato del codice scritto in Riverpod.
  • debugging migliore, attraverso la generazione di metadata extra che il debugger poi riconosce.
  • alcune feature di Riverpod saranno disponibili solo con la generazione di codice.

La sintassi

Definire un provider:

Quando si definisce un provider usando la generazione di codice, è utile tenersi a mente i seguenti punti:

  • I provider possono essere definiti come una funzione o una classe annotata. Sono pressochè la stessa cosa, ma il provider basato sulla classe ha il vantaggio di includere i metodi pubblici che abilitano oggetti esterni di modificare lo stato del provider (side-effects). I provider basati sulla funzione aiutano ad avere una sintassi più compatta al posto di scrivere un provider "Class-based" che ha come metodo solo `build`, e come tale, non può essere modificato dall'interfaccia grafica.
  • Tutti gli async primitivi di Dart (Future, FutureOr e Stream) sono supportati.
  • Quando una funzione è marcata come async, il provider gestisce automaticamente gli stati di errore/caricamento ed espone un AsyncValue.
Funzionale
(Non può performare side-effects
usando metodi pubblici)
Basato su Classe
(Può performare side-effects
usando metodi pubblici)
Sync

String example(ExampleRef ref) {
return 'foo';
}

class Example extends _$Example {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
Async - Future

Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}

class Example extends _$Example {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
Async - Stream

Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}

class Example extends _$Example {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}

Abilitare/Disabilitare autoDispose:

Quando si usa la generazione di codice, i provider sono autoDispose di default. Ciò significa che si distruggeranno automaticamente da soli quando non ci saranno più ascoltatori collegati (ref.watch/ref.listen). Questa impostazione predefinita si allinea meglio con la filosofia di Riverpod. Inizialmente con la variante senza generazione di codice, autoDispose era disattivo di default per accogliere gli utenti che migravano da package:provider.

Se desideri disabilitare autoDispose, puoi farlo passando keepAlive: true all'annotazione.

// AutoDispose provider (keepAlive is false by default)

String example1(Example1Ref ref) => 'foo';

// Non autoDispose provider
(keepAlive: true)
String example2(Example2Ref ref) => 'foo';

Passare parametri ad un provider (family):

Quando si usa la generazione di codice, non dobbiamo più fare affidamento sul modificatore family per passare dei parametri ad un provider. Invece, la funzione principale del nostro provider può accettare qualsiasi numero di parametri, inclusi i parametri nominali, opzionali, o con valori di default. Tuttavia, nota che questi parametri dovrebbero comunque avere un == coerente. Ciò significa che o i valori dovrebbero essere memorizzati nella cache, o i parametri dovrebbero sovrascrivere ==.

FunzionaleBasato su Classe

String example(
ExampleRef ref,
int param1, {
String param2 = 'foo',
}) {
return 'Hello $param1 & param2';
}

class Example extends _$Example {

String build(
int param1, {
String param2 = 'foo',
}) {
return 'Hello $param1 & param2';
}

// Add methods to mutate the state
}

Migrare dalla variante senza generazione di codice:

Quando non si usa la variante senza generazione di codice è necessario determinare manualmente la tipologia del tuo provider. Di seguito le opzioni corrispondenti per passare alla variante di generazione del codice:

Provider
Prima
final exampleProvider = Provider.autoDispose<String>(
(ref) {
return 'foo';
},
);
Dopo

String example(ExampleRef ref) {
return 'foo';
}
NotifierProvider
Prima
final exampleProvider = NotifierProvider.autoDispose<ExampleNotifier, String>(
ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeNotifier<String> {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
Dopo

class Example extends _$Example {

String build() {
return 'foo';
}

// Add methods to mutate the state
}
FutureProvider
Prima
final exampleProvider =
FutureProvider.autoDispose<String>((ref) async {
return Future.value('foo');
});
Dopo

Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}
StreamProvider
Prima
final exampleProvider =
StreamProvider.autoDispose<String>((ref) async* {
yield 'foo';
});
Dopo

Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}
AsyncNotifierProvider
Prima
final exampleProvider =
AsyncNotifierProvider.autoDispose<ExampleNotifier, String>(
ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeAsyncNotifier<String> {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
Dopo

class Example extends _$Example {

Future<String> build() async {
return Future.value('foo');
}

// Add methods to mutate the state
}
StreamNotifierProvider
Prima
final exampleProvider =
StreamNotifierProvider.autoDispose<ExampleNotifier, String>(() {
return ExampleNotifier();
});

class ExampleNotifier extends AutoDisposeStreamNotifier<String> {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}
Dopo

class Example extends _$Example {

Stream<String> build() async* {
yield 'foo';
}

// Add methods to mutate the state
}