Riverpod Deep Dive: AsyncNotifier, generování kódu a testování
Riverpod 3.0 představil tři vylepšení, která mění způsob psaní
Aplikace Flutter: AsyncNotifier pro správu asynchronních stavů
s úplnou typovou bezpečností, generování kódu con riverpod_generator
který eliminuje opakující se kotrmelce a systém testování založené
na přepsání poskytovatele, které vám umožní otestovat jakýkoli scénář bez simulací
externí rámce.
V této příručce vytváříme kompletní funkci — seznam článků s vyhledáváním, stránkování a oblíbené položky – počínaje úložištěm až po widget, s testy jednotek, Testování widgetů a robustní zpracování chyb. Kód je připraven k výrobě.
Co se naučíte
- AsyncNotifier: typově bezpečná správa stavu načítání/dat/chyb
- riverpod_generator: @riverpod anotace a vygenerovaný kód
- Rodinní poskytovatelé: dynamické parametry u poskytovatelů
- Složení poskytovatelů: jak jsou poskytovatelé na sobě závislí
- ref.watch vs ref.read vs ref.listen: kdy použít který
- Testování pomocí ProviderContainer a přepsání
- Testování widgetů pomocí ProviderScope a selektivních přepisů
Nastavení projektu s generováním kódu 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: Správa asynchronních stavů
// 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);
}
}
}
Rodinní poskytovatelé: Parametrizovaní poskytovatelé
// 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.read vs ref.listen
// 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();
}
}
Testování s Riverpod: Override a 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);
});
}
Závěry
Riverpod 3.0 s generováním kódu představuje pro stát nejmodernější Správa flutteru: AsyncNotifier elegantně řídí životní cyklus asynchronních stavů generátor kódu eliminuje základní desku bez obětování bezpečnost typu a systém potlačení umožňuje testování izolované a rychlé bez závisí na složitých zesměšňovacích rámcích.







