Riverpod Deep Dive: AsyncNotifier, generare de cod și testare
Riverpod 3.0 a introdus trei îmbunătățiri care schimbă modul în care scrieți
Aplicații Flutter: AsyncNotifier pentru gestionarea stărilor asincrone
cu siguranță de tip complet, generarea codului cu riverpod_generator
care elimină boilerplate repetitive și un sistem de testarea bazat
la anularea furnizorului care vă permite să testați orice scenariu fără imitații
cadre externe.
În acest ghid construim o caracteristică completă - o listă de articole cu căutare, paginare și favorite — începând de la depozit până la widget, cu teste unitare, Testarea widget-urilor și gestionarea robustă a erorilor. Codul este gata pentru producție.
Ce vei învăța
- AsyncNotifier: gestionarea stării de încărcare/date/erori în siguranță
- riverpod_generator: adnotări @riverpod și cod generat
- Furnizorii de familie: parametri dinamici în furnizori
- Compoziția furnizorilor: cum depind furnizorii unul de celălalt
- ref.watch vs ref.read vs ref.listen: când să folosești care
- Testarea cu ProviderContainer și override
- Testarea widget-urilor cu ProviderScope și înlocuiri selective
Configurarea proiectului cu generarea codului Riverpod
// pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
riverpod_generator: ^3.0.0
riverpod_lint: ^2.0.0
build_runner: ^2.4.0
flutter_test:
sdk: flutter
// Genera il codice:
// flutter pub run build_runner build --delete-conflicting-outputs
// Oppure in watch mode durante lo sviluppo:
// flutter pub run build_runner watch --delete-conflicting-outputs
// analysis_options.yaml: abilita riverpod_lint
analyzer:
plugins:
- custom_lint
custom_lint:
rules:
- riverpod_lint
AsyncNotifier: gestionarea stărilor asincrone
// Dominio: modelli
class Article {
final String id;
final String title;
final String excerpt;
final String category;
final bool isFavorite;
const Article({
required this.id,
required this.title,
required this.excerpt,
required this.category,
required this.isFavorite,
});
Article copyWith({bool? isFavorite}) => Article(
id: id,
title: title,
excerpt: excerpt,
category: category,
isFavorite: isFavorite ?? this.isFavorite,
);
}
// Repository (interfaccia)
abstract class ArticleRepository {
Future<List<Article>> getArticles({
String? query,
String? category,
int page = 1,
});
Future<void> toggleFavorite(String articleId);
}
// article_repository.dart
part 'article_repository.g.dart';
@riverpod
ArticleRepository articleRepository(Ref ref) {
// In test: override con mock
// In produzione: usa l'implementazione reale
return RemoteArticleRepository(
dio: ref.watch(dioProvider),
);
}
// articles_provider.dart: AsyncNotifier con code gen
part 'articles_provider.g.dart';
@riverpod
class ArticlesController extends _$ArticlesController {
@override
Future<List<Article>> build() async {
// Chiamato automaticamente all'inizializzazione
return _fetchArticles();
}
Future<List<Article>> _fetchArticles({
String? query,
String? category,
}) async {
return ref.read(articleRepositoryProvider).getArticles(
query: query,
category: category,
);
}
// Azione: ricerca
Future<void> search(String query) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(
() => _fetchArticles(query: query),
);
}
// Azione: toggle preferito (ottimistic update)
Future<void> toggleFavorite(String articleId) async {
// 1. Update ottimistico: cambia immediatamente nell'UI
state = state.whenData(
(articles) => articles.map((a) =>
a.id == articleId ? a.copyWith(isFavorite: !a.isFavorite) : a
).toList(),
);
// 2. Persisti sul server
try {
await ref.read(articleRepositoryProvider).toggleFavorite(articleId);
} catch (e) {
// 3. Rollback se fallisce
state = await AsyncValue.guard(_fetchArticles);
}
}
}
Furnizori de familie: Furnizori parametrizați
// article_detail_provider.dart: provider con parametro ID
part 'article_detail_provider.g.dart';
@riverpod
Future<Article> articleDetail(Ref ref, String articleId) async {
// Parametro articleId: ogni ID ha il suo provider separato
// La cache e indipendente per ogni ID
return ref.watch(articleRepositoryProvider).getArticleById(articleId);
}
// Utilizzo nel widget:
class ArticleDetailPage extends ConsumerWidget {
final String articleId;
const ArticleDetailPage({required this.articleId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// articleDetailProvider e un "family" provider
// ref.watch(articleDetailProvider(articleId)) =>
// ogni ID ha il suo AsyncValue separato
final articleAsync = ref.watch(articleDetailProvider(articleId));
return articleAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
children: [
Text('Errore: $error'),
ElevatedButton(
onPressed: () => ref.refresh(articleDetailProvider(articleId)),
child: const Text('Riprova'),
),
],
),
),
data: (article) => ArticleDetailContent(article: article),
);
}
}
ref.watch vs ref.citește vs ref.ascultă
// Le tre modalita di accesso ai provider in Riverpod
class ExampleWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch: REAGISCE ai cambiamenti
// Usa dentro build(): ricostruisce il widget quando cambia
// NON usare in callback asincroni (il widget potrebbe essere smontato)
final articles = ref.watch(articlesControllerProvider);
return Column(
children: [
ElevatedButton(
onPressed: () async {
// ref.read: LEGGE senza ascoltare
// Usa per azioni one-shot (tap, submit form)
// NON usa dentro build() per la logica reattiva
await ref.read(articlesControllerProvider.notifier).search('flutter');
},
child: const Text('Cerca Flutter'),
),
],
);
}
}
// ref.listen: ASCOLTA i cambiamenti per side effects
// Usa per navigazione, snackbar, dialog - non per ricostruire il widget
class ArticleListPage extends ConsumerStatefulWidget {
@override
ConsumerState<ArticleListPage> createState() => _ArticleListPageState();
}
class _ArticleListPageState extends ConsumerState<ArticleListPage> {
@override
void initState() {
super.initState();
// ref.listen: eseguito ogni volta che lo stato cambia
// NON e asincrono: viene chiamato durante il build cycle
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.listenManual(articlesControllerProvider, (previous, next) {
if (next is AsyncError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore: ${next.error}')),
);
}
});
});
}
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
Testarea cu Riverpod: Override și ProviderContainer
// test/articles_controller_test.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
// Mock del repository
class MockArticleRepository extends Mock implements ArticleRepository {}
void main() {
late MockArticleRepository mockRepo;
late ProviderContainer container;
setUp(() {
mockRepo = MockArticleRepository();
// ProviderContainer: istanza isolata per test (nessun singleton)
container = ProviderContainer(
overrides: [
// Override: sostituisce il repository reale con il mock
articleRepositoryProvider.overrideWithValue(mockRepo),
],
);
});
tearDown(() {
// IMPORTANTE: dispose per liberare risorse
container.dispose();
});
test('inizializza con articoli dal repository', () async {
final testArticles = [
Article(id: '1', title: 'Test', excerpt: 'Ex', category: 'Dev', isFavorite: false),
];
when(mockRepo.getArticles()).thenAnswer((_) async => testArticles);
// Legge il provider: avvia il build() di AsyncNotifier
final result = await container.read(
articlesControllerProvider.future,
);
expect(result, equals(testArticles));
verify(mockRepo.getArticles()).called(1);
});
test('search aggiorna la lista', () async {
final allArticles = [
Article(id: '1', title: 'Flutter', excerpt: '', category: 'Dev', isFavorite: false),
Article(id: '2', title: 'Angular', excerpt: '', category: 'Web', isFavorite: false),
];
final filteredArticles = [allArticles[0]];
when(mockRepo.getArticles()).thenAnswer((_) async => allArticles);
when(mockRepo.getArticles(query: 'Flutter')).thenAnswer((_) async => filteredArticles);
// Aspetta il caricamento iniziale
await container.read(articlesControllerProvider.future);
// Esegui la ricerca
await container.read(articlesControllerProvider.notifier).search('Flutter');
final results = await container.read(articlesControllerProvider.future);
expect(results, equals(filteredArticles));
});
// Widget test con override
testWidgets('mostra loading indicator durante fetch', (tester) async {
// Simula caricamento lento
when(mockRepo.getArticles()).thenAnswer(
(_) async {
await Future.delayed(const Duration(seconds: 1));
return [];
},
);
await tester.pumpWidget(
ProviderScope(
overrides: [
articleRepositoryProvider.overrideWithValue(mockRepo),
],
child: const MaterialApp(home: ArticleListPage()),
),
);
// Primo frame: loading
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Aspetta il completamento
await tester.pumpAndSettle();
expect(find.byType(CircularProgressIndicator), findsNothing);
});
}
Concluzii
Riverpod 3.0 cu generare de cod reprezintă stadiul tehnicii pentru stat Gestionarea flutterului: AsyncNotifier gestionează în mod elegant ciclul de viață de stări asincrone, generatorul de cod elimină boilerplate fără a sacrifica siguranța de tip, iar sistemul de anulare face testarea izolată și rapidă fără depind de cadre complexe batjocoritoare.







