Riverpod の詳細: AsyncNotifier、コード生成、テスト
Riverpod 3.0 では、書き方を変える 3 つの改善点が導入されました。
フラッターアプリ: AsyncNotifier 非同期状態の管理用
完全なタイプセーフティを備え、 コード生成 con riverpod_generator
これにより、反復的な定型文が排除され、 テスト ベースの
モックなしであらゆるシナリオをテストできるプロバイダー オーバーライドについて
外部フレームワーク。
このガイドでは、完全な機能、つまり検索機能を備えた記事のリストを構築します。 ページネーションとお気に入り — リポジトリから単体テストを含むウィジェットまで、 ウィジェットのテストと堅牢なエラー処理。コードは実稼働の準備ができています。
何を学ぶか
- AsyncNotifier: タイプセーフなロード/データ/エラーステータス管理
- Riverpod_generator: @riverpod アノテーションと生成されたコード
- ファミリ プロバイダ: プロバイダの動的パラメータ
- プロバイダーの構成: プロバイダーがどのように相互に依存するか
- ref.watch vs ref.read vs ref.listen: いつどれを使用するか
- ProviderContainer とオーバーライドを使用したテスト
- ProviderScope と選択的オーバーライドを使用したウィジェットのテスト
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: 非同期状態の管理
// 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);
}
}
}
ファミリ プロバイダー: パラメーター化されたプロバイダー
// 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();
}
}
Riverpod を使用したテスト: Override と 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);
});
}
結論
コード生成を備えた Riverpod 3.0 は州の最先端技術を表します Flutter 管理: AsyncNotifier はライフサイクルをエレガントに管理します 非同期状態のコード ジェネレーターは、犠牲にすることなくボイラープレートを排除します。 タイプセーフティとオーバーライドシステムにより、テストを分離して迅速に行うことができます。 複雑なモック フレームワークに依存します。







