Riverpod 3.0 Provider Architecture
This directory contains core-level providers that are used across the application.
Directory Structure
lib/core/providers/
├── connectivity_provider.dart # Network connectivity monitoring
├── provider_examples.dart # Comprehensive Riverpod 3.0 examples
└── README.md # This file
Setup Instructions
1. Install Dependencies
Dependencies are already configured in pubspec.yaml:
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
build_runner: ^2.4.11
riverpod_generator: ^3.0.0
riverpod_lint: ^3.0.0
custom_lint: ^0.7.0
Run to install:
flutter pub get
2. Generate Code
Run code generation whenever you create or modify providers:
# Watch mode (auto-regenerates on changes)
dart run build_runner watch -d
# One-time build
dart run build_runner build -d
# Clean and rebuild
dart run build_runner build --delete-conflicting-outputs
3. Wrap App with ProviderScope
In main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
Riverpod 3.0 Key Concepts
@riverpod Annotation
The @riverpod annotation is the core of code generation. It automatically generates the appropriate provider type based on your function/class signature.
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'my_provider.g.dart';
// Simple value
@riverpod
String myValue(MyValueRef ref) => 'Hello';
// Async value
@riverpod
Future<String> myAsync(MyAsyncRef ref) async => 'Hello';
// Stream
@riverpod
Stream<int> myStream(MyStreamRef ref) => Stream.value(1);
// Mutable state
@riverpod
class MyNotifier extends _$MyNotifier {
@override
int build() => 0;
void increment() => state++;
}
// Async mutable state
@riverpod
class MyAsyncNotifier extends _$MyAsyncNotifier {
@override
Future<String> build() async => 'Initial';
Future<void> update(String value) async {
state = await AsyncValue.guard(() async => value);
}
}
Provider Types (Auto-Generated)
-
Simple Provider - Immutable value
- Function returning T → Provider
-
FutureProvider - Async value
- Function returning Future → FutureProvider
-
StreamProvider - Stream of values
- Function returning Stream → StreamProvider
-
NotifierProvider - Mutable state with methods
- Class extending Notifier → NotifierProvider
-
AsyncNotifierProvider - Async mutable state
- Class extending AsyncNotifier → AsyncNotifierProvider
-
StreamNotifierProvider - Stream mutable state
- Class extending StreamNotifier → StreamNotifierProvider
Family (Parameters)
In Riverpod 3.0, family is just function parameters!
// Old way (Riverpod 2.x)
final userProvider = FutureProvider.family<User, String>((ref, id) async {
return fetchUser(id);
});
// New way (Riverpod 3.0)
@riverpod
Future<User> user(UserRef ref, String id) async {
return fetchUser(id);
}
// Multiple parameters (named, optional, defaults)
@riverpod
Future<List<Post>> posts(
PostsRef ref, {
required String userId,
int page = 1,
int limit = 20,
String? category,
}) async {
return fetchPosts(userId, page, limit, category);
}
// Usage
ref.watch(userProvider('user123'));
ref.watch(postsProvider(userId: 'user123', page: 2, category: 'tech'));
AutoDispose vs KeepAlive
// AutoDispose (default) - cleaned up when not used
@riverpod
String autoExample(AutoExampleRef ref) => 'Auto disposed';
// KeepAlive - stays alive until app closes
@Riverpod(keepAlive: true)
String keepExample(KeepExampleRef ref) => 'Kept alive';
Unified Ref Type
Riverpod 3.0 uses a single Ref type (no more FutureProviderRef, StreamProviderRef, etc.):
@riverpod
Future<String> example(ExampleRef ref) async {
// All providers use the same ref type
ref.watch(otherProvider);
ref.read(anotherProvider);
ref.listen(thirdProvider, (prev, next) {});
ref.invalidate(fourthProvider);
}
ref.mounted Check
Always check ref.mounted after async operations in Notifiers:
@riverpod
class Example extends _$Example {
@override
String build() => 'Initial';
Future<void> updateData() async {
await Future.delayed(Duration(seconds: 2));
// Check if provider is still mounted
if (!ref.mounted) return;
state = 'Updated';
}
}
Error Handling with AsyncValue.guard()
@riverpod
class Data extends _$Data {
@override
Future<String> build() async => 'Initial';
Future<void> update(String value) async {
state = const AsyncValue.loading();
// AsyncValue.guard catches errors automatically
state = await AsyncValue.guard(() async {
await api.update(value);
return value;
});
}
}
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
void initState() {
super.initState();
// ref is available in all lifecycle methods
}
@override
Widget build(BuildContext context) {
final value = ref.watch(myProvider);
return Text(value);
}
}
Consumer (for optimization)
Column(
children: [
const Text('Static'),
Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('$count');
},
),
],
)
Best Practices
1. Use .select() for Optimization
// 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));
// Good - rebuilds only when async value has data
final userName = ref.watch(
userProvider.select((async) => async.value?.name),
);
2. Provider Dependencies
@riverpod
Future<Dashboard> dashboard(DashboardRef ref) async {
// Watch other providers
final user = await ref.watch(userProvider.future);
final posts = await ref.watch(postsProvider.future);
return Dashboard(user: user, posts: posts);
}
3. Invalidation and Refresh
// In a widget or notifier
ref.invalidate(userProvider); // Invalidate
ref.refresh(userProvider); // Invalidate and re-read
4. Lifecycle Hooks
@riverpod
String example(ExampleRef ref) {
ref.onDispose(() {
// Clean up
});
ref.onCancel(() {
// Last listener removed
});
ref.onResume(() {
// New listener added after cancel
});
return 'value';
}
5. 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);
});
test('async provider', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
final value = await container.read(userProvider.future);
expect(value.name, 'John');
});
Migration from Riverpod 2.x
StateNotifierProvider → NotifierProvider
// Old (2.x)
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
// New (3.0)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
FutureProvider.family → Function with Parameters
// Old (2.x)
final userProvider = FutureProvider.family<User, String>((ref, id) async {
return fetchUser(id);
});
// New (3.0)
@riverpod
Future<User> user(UserRef ref, String id) async {
return fetchUser(id);
}
Ref Types → Single Ref
// Old (2.x)
final provider = FutureProvider<String>((FutureProviderRef ref) async {
return 'value';
});
// New (3.0)
@riverpod
Future<String> provider(ProviderRef ref) async {
return 'value';
}
Common Patterns
See provider_examples.dart for comprehensive examples of:
- Simple providers
- Async providers (FutureProvider pattern)
- Stream providers
- Notifier (mutable state)
- AsyncNotifier (async mutable state)
- StreamNotifier
- Family (parameters)
- Provider composition
- Error handling
- Lifecycle hooks
- Optimization with .select()
Riverpod Lint Rules
The project is configured with riverpod_lint for additional checks:
provider_dependencies- Ensure proper dependency usagescoped_providers_should_specify_dependencies- Scoped provider safetyavoid_public_notifier_properties- Encapsulationavoid_ref_read_inside_build- Performanceavoid_manual_providers_as_generated_provider_dependency- Use generated providersfunctional_ref- Proper ref usagenotifier_build- Proper Notifier implementation
Run linting:
dart run custom_lint