Perché Riverpod?
La gestione dello stato è uno dei temi più dibattuti nello sviluppo Flutter. Tra le tante soluzioni disponibili (setState, Provider, Bloc, GetX), Riverpod si è imposto come una delle scelte più solide e moderne, soprattutto a partire dalla versione 2.0.
Riverpod nasce dallo stesso autore di Provider (Remi Rousselet) e ne risolve i principali limiti:
- È compile-safe: gli errori vengono individuati a tempo di compilazione, non a runtime.
- Non dipende dal
BuildContext, quindi puoi accedere allo stato anche al di fuori del widget tree. - Supporta la code generation, riducendo il boilerplate.
Installazione
Aggiungi le dipendenze al tuo pubspec.yaml:
dependencies:
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
build_runner: ^2.4.11
riverpod_generator: ^2.4.3
Per abilitare Riverpod nell'intera app, avvolgi il widget radice con ProviderScope:
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
Il primo provider con code generation
Con Riverpod 2.0 possiamo usare l'annotazione @riverpod per generare automaticamente i provider. Ecco un semplice provider che restituisce un valore:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'greeting.g.dart';
@riverpod
String greeting(GreetingRef ref) {
return 'Ciao da Riverpod!';
}
Dopo aver lanciato il comando di build:
dart run build_runner watch -d
verrà generato il file greeting.g.dart con il provider greetingProvider.
Leggere lo stato nei widget
Per accedere allo stato, il widget deve estendere ConsumerWidget e usare l'oggetto ref:
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Scaffold(
body: Center(child: Text(greeting)),
);
}
}
Stato mutabile con Notifier
Per gestire uno stato che cambia nel tempo (ad esempio un contatore), usiamo la classe Notifier annotata:
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
Nel widget possiamo sia osservare lo stato sia richiamare i metodi:
class CounterView extends ConsumerWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Valore: $count', style: const TextStyle(fontSize: 24)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () =>
ref.read(counterProvider.notifier).decrement(),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () =>
ref.read(counterProvider.notifier).increment(),
),
],
),
],
);
}
}
Nota: usa
ref.watchper ricostruire la UI quando lo stato cambia, mentreref.readper chiamare metodi senza creare una dipendenza.
Gestire dati asincroni
Uno dei punti di forza di Riverpod è la gestione nativa dell'asincronia tramite AsyncValue e FutureProvider:
@riverpod
Future<List<String>> users(UsersRef ref) async {
final response = await fetchUsersFromApi();
return response;
}
Nel widget, AsyncValue ci permette di gestire elegantemente i tre stati (dati, caricamento, errore):
class UsersList extends ConsumerWidget {
const UsersList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
data: (users) => ListView(
children: users.map((u) => ListTile(title: Text(u))).toList(),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Errore: $err')),
);
}
}
Conclusioni
Riverpod 2.0 offre un approccio dichiarativo, type-safe e scalabile alla gestione dello stato. Grazie alla code generation riduce drasticamente il boilerplate e rende il codice più leggibile e manutenibile. Se stai iniziando un nuovo progetto Flutter, è senza dubbio una delle soluzioni più consigliate per il 2024.