Riverpod Derin İncelemesi: AsyncNotifier, Kod Oluşturma ve Test Etme
Riverpod 3.0, yazma şeklinizi değiştiren üç iyileştirme sundu
Flutter'ın uygulamaları: AsyncNotifier asenkron durumları yönetmek için
tam tip emniyetli, kod üretimi ile riverpod_generator
Tekrarlanan standart kalıpları ortadan kaldıran ve bir sistem olan test dayalı
herhangi bir senaryoyu sahte olmadan test etmenize olanak tanıyan sağlayıcı geçersiz kılmada
dış çerçeveler.
Bu kılavuzda eksiksiz bir özellik oluşturuyoruz: arama içeren makalelerin listesi, sayfalandırma ve sık kullanılanlar - depodan başlayarak birim testleriyle widget'a kadar, Widget testi ve güçlü hata yönetimi. Kod üretime hazır.
Ne Öğreneceksiniz
- AsyncNotifier: tür açısından güvenli yükleme/veri/hata durumu yönetimi
- riverpod_generator: @riverpod ek açıklamaları ve oluşturulan kod
- Aile sağlayıcıları: sağlayıcılardaki dinamik parametreler
- Sağlayıcı bileşimi: sağlayıcıların birbirlerine nasıl bağımlı olduğu
- ref.watch vs ref.read vs ref.listen: ne zaman kullanılmalı hangisi
- ProviderContainer ile test etme ve geçersiz kılma
- ProviderScope ve seçici geçersiz kılmalarla widget testi
Riverpod Kod Oluşturma ile Proje Kurulumu
// 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: Eşzamansız Durumların Yönetimi
// 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);
}
}
}
Aile Sağlayıcıları: Parametreli Sağlayıcılar
// 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 ile Test Etme: Override ve 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);
});
}
Sonuçlar
Kod oluşturma özelliğine sahip Riverpod 3.0, eyalet için en son teknolojiyi temsil ediyor Çarpıntı yönetimi: AsyncNotifier yaşam döngüsünü zarif bir şekilde yönetir Asenkron durumların çözümü için kod oluşturucu, standartlardan ödün vermeden standart kalıpları ortadan kaldırır. tip güvenliği ve geçersiz kılma sistemi, testleri izole edilmiş ve hızlı hale getirir. karmaşık alaycı çerçevelere bağlıdır.







