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.watch per ricostruire la UI quando lo stato cambia, mentre ref.read per 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.