8.7 KiB
8.7 KiB
Riverpod 3.0 Quick Reference Card
File Structure
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'my_provider.g.dart'; // REQUIRED!
// Your providers here
Provider Types
1. Simple Value (Immutable)
@riverpod
String appName(AppNameRef ref) => 'Worker App';
// Usage
final name = ref.watch(appNameProvider);
2. Async Value (Future)
@riverpod
Future<User> user(UserRef ref, String id) async {
return await fetchUser(id);
}
// Usage
final userAsync = ref.watch(userProvider('123'));
userAsync.when(
data: (user) => Text(user.name),
loading: () => const CustomLoadingIndicator(),
error: (e, _) => Text('Error: $e'),
);
3. Stream
@riverpod
Stream<Message> messages(MessagesRef ref) {
return webSocket.messages;
}
// Usage
final messages = ref.watch(messagesProvider);
4. Mutable State (Notifier)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
// Usage
final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).increment();
5. Async Mutable State (AsyncNotifier)
@riverpod
class Profile extends _$Profile {
@override
Future<User> build() async {
return await api.getProfile();
}
Future<void> update(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await api.updateProfile(name);
});
}
}
// Usage
final profile = ref.watch(profileProvider);
await ref.read(profileProvider.notifier).update('New Name');
Family (Parameters)
// Single parameter
@riverpod
Future<Post> post(PostRef ref, String id) async {
return await api.getPost(id);
}
// Multiple parameters
@riverpod
Future<List<Post>> posts(
PostsRef ref, {
required String userId,
int page = 1,
String? category,
}) async {
return await api.getPosts(userId, page, category);
}
// Usage
ref.watch(postProvider('post-123'));
ref.watch(postsProvider(userId: 'user-1', page: 2));
AutoDispose vs KeepAlive
// AutoDispose (default) - cleaned up when not used
@riverpod
String temp(TempRef ref) => 'Auto disposed';
// KeepAlive - stays alive
@Riverpod(keepAlive: true)
String config(ConfigRef ref) => 'Global config';
Usage in Widgets
ConsumerWidget
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(myProvider);
return Text(value);
}
}
ConsumerStatefulWidget
class MyWidget extends ConsumerStatefulWidget {
@override
ConsumerState<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends ConsumerState<MyWidget> {
@override
Widget build(BuildContext context) {
final value = ref.watch(myProvider);
return Text(value);
}
}
Consumer (optimization)
Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('$count');
},
)
Ref Methods
ref.watch() - Use in build
// Rebuilds when value changes
final value = ref.watch(myProvider);
ref.read() - Use in callbacks
// One-time read, doesn't listen
onPressed: () {
ref.read(myProvider.notifier).update();
}
ref.listen() - Side effects
ref.listen(authProvider, (prev, next) {
if (next.isLoggedOut) {
Navigator.of(context).pushReplacementNamed('/login');
}
});
ref.invalidate() - Force refresh
ref.invalidate(userProvider);
ref.refresh() - Invalidate and read
final newValue = ref.refresh(userProvider);
AsyncValue Handling
.when()
asyncValue.when(
data: (value) => Text(value),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'),
);
Pattern Matching (Dart 3+)
switch (asyncValue) {
case AsyncData(:final value):
return Text(value);
case AsyncError(:final error):
return Text('Error: $error');
case AsyncLoading():
return const CustomLoadingIndicator();
}
Direct Checks
if (asyncValue.isLoading) return Loading();
if (asyncValue.hasError) return Error();
final data = asyncValue.value!;
Performance Optimization
Use .select()
// Bad - rebuilds on any user change
final user = ref.watch(userProvider);
// Good - rebuilds only when name changes
final name = ref.watch(
userProvider.select((user) => user.name),
);
// With AsyncValue
final name = ref.watch(
userProvider.select((async) => async.value?.name),
);
Error Handling
AsyncValue.guard()
@riverpod
class Data extends _$Data {
@override
Future<String> build() async => 'Initial';
Future<void> update(String value) async {
state = const AsyncValue.loading();
// Catches errors automatically
state = await AsyncValue.guard(() async {
return await api.update(value);
});
}
}
Provider Composition
@riverpod
Future<Dashboard> dashboard(DashboardRef ref) async {
// Depend on other providers
final user = await ref.watch(userProvider.future);
final posts = await ref.watch(postsProvider.future);
return Dashboard(user: user, posts: posts);
}
Lifecycle Hooks
@riverpod
String example(ExampleRef ref) {
ref.onDispose(() {
// Cleanup
print('Disposed');
});
ref.onCancel(() {
// Last listener removed
});
ref.onResume(() {
// New listener added
});
return 'value';
}
ref.mounted Check (Riverpod 3.0)
@riverpod
class Example extends _$Example {
@override
String build() => 'Initial';
Future<void> update() async {
await Future.delayed(Duration(seconds: 2));
// Check if still mounted
if (!ref.mounted) return;
state = 'Updated';
}
}
Code Generation Commands
# Watch mode (recommended)
dart run build_runner watch -d
# One-time build
dart run build_runner build --delete-conflicting-outputs
# Clean and rebuild
dart run build_runner clean && dart run build_runner build -d
Linting
# Check Riverpod issues
dart run custom_lint
# Auto-fix
dart run custom_lint --fix
Testing
test('counter increments', () {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(counterProvider), 0);
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
Widget Testing
testWidgets('test', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWith((ref) => User(name: 'Test')),
],
child: MaterialApp(home: MyScreen()),
),
);
expect(find.text('Test'), findsOneWidget);
});
Best Practices
✅ DO:
- Use
ref.watch()in build methods - Use
ref.read()in event handlers - Use
.select()to optimize rebuilds - Check
ref.mountedafter async operations - Use
AsyncValue.guard()for error handling - Use autoDispose for temporary state
- Keep providers in dedicated directories
❌ DON'T:
- Use
ref.read()in build methods - Forget the
partdirective - Use deprecated
StateNotifierProvider - Create providers without code generation
- Forget to run build_runner after changes
Common Patterns
Loading State
Future<void> save() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.save());
}
Pagination
@riverpod
class PostList extends _$PostList {
@override
Future<List<Post>> build() => _fetch(0);
int _page = 0;
Future<void> loadMore() async {
final current = state.value ?? [];
_page++;
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final newPosts = await _fetch(_page);
return [...current, ...newPosts];
});
}
Future<List<Post>> _fetch(int page) async {
return await api.getPosts(page: page);
}
}
Form State
@riverpod
class LoginForm extends _$LoginForm {
@override
LoginFormState build() => LoginFormState();
void setEmail(String email) {
state = state.copyWith(email: email);
}
void setPassword(String password) {
state = state.copyWith(password: password);
}
Future<void> submit() async {
if (!state.isValid) return;
state = state.copyWith(isLoading: true);
try {
await api.login(state.email, state.password);
state = state.copyWith(isLoading: false, success: true);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
}
Resources
- 📄 provider_examples.dart - All patterns with examples
- 📄 connectivity_provider.dart - Real-world implementation
- 📄 RIVERPOD_SETUP.md - Complete guide
- 🌐 https://riverpod.dev - Official documentation