Hooks
This guide demonstrates how to migrate from traditional state management to hooks_riverpod in your FlipDare project.
- Automatic Memory Management: No need to manually dispose controllers or streams
- Built-in Caching: Automatic caching with intelligent invalidation
- Offline Support: Built-in support for offline mode and data persistence
- Type Safety: Full type safety with compile-time checks
- Testability: Easy to test with provider overrides
- Performance: Optimized rebuilds only when needed
- Code Generation: Automatic provider generation with less boilerplate
class OldPledgeWidget extends StatefulWidget {
@override
_OldPledgeWidgetState createState() => _OldPledgeWidgetState();
}
class _OldPledgeWidgetState extends State<OldPledgeWidget> {
PledgeService? _pledgeService;
List<PledgeModel>? _pledges;
bool _loading = true;
String? _error;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_initializeService();
}
void _initializeService() async {
try {
_pledgeService = await PledgeService.create();
_subscription = await _pledgeService!.getMyPledges().listen(
(snapshot) {
setState(() {
_pledges = snapshot.docs.map((doc) =>
PledgeModel.fromJson(doc.data(), id: doc.id)
).toList();
_loading = false;
});
},
onError: (error) {
setState(() {
_error = error.toString();
_loading = false;
});
},
);
} catch (e) {
setState(() {
_error = e.toString();
_loading = false;
});
}
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_loading) return CircularProgressIndicator();
if (_error != null) return Text('Error: $_error');
return ListView.builder(/* ... */);
}
}
class NewPledgeWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final pledgesAsync = ref.watch(myPledgesStreamProvider);
return pledgesAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (pledges) => ListView.builder(/* ... */),
);
}
}
lib/
โโโ main_riverpod_example.dart # Entry point
โโโ providers/
โ โโโ pledge_providers.dart # Riverpod providers (with codegen)
โ โโโ pledge_providers.g.dart # Generated providers
โโโ pages/
โโโ firebase/
โโโ find_pledges.dart # Full-featured example
โโโ find_pledges_simple.dart # Simple example
Add to your pubspec.yaml:
dependencies:
flutter_hooks: ^0.21.0
hooks_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
dev_dependencies:
riverpod_generator: ^2.6.3
riverpod_lint: ^2.6.3
build_runner: ^2.5.4
dart run build_runner build
void main() {
runApp(
ProviderScope( // Required for Riverpod
child: MyApp(),
),
);
}
final searchQueryProvider = StateProvider<String>((ref) => '');
// Usage
final searchQuery = ref.watch(searchQueryProvider);
ref.read(searchQueryProvider.notifier).state = 'new value';
@riverpod
Future<List<PledgeModel>> pledges(PledgesRef ref) async {
final service = ref.watch(pledgeServiceProvider);
return await service.getAllPledges();
}
// Usage
final pledgesAsync = ref.watch(pledgesProvider);
@riverpod
Stream<List<PledgeModel>> pledgesStream(PledgesStreamRef ref) async* {
final service = ref.watch(pledgeServiceProvider);
final stream = await service.getMyPledgesStream();
await for (final snapshot in stream) {
yield snapshot.docs.map((doc) =>
PledgeModel.fromJson(doc.data(), id: doc.id)
).toList();
}
}
@riverpod
class PledgeManager extends _$PledgeManager {
@override
AsyncValue<List<PledgeModel>> build() {
return const AsyncValue.loading();
}
Future<void> loadPledges() async {
state = const AsyncValue.loading();
try {
final pledges = await _fetchPledges();
state = AsyncValue.data(pledges);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
Future<void> addPledge(PledgeModel pledge) async {
// Optimistic update
state.whenData((pledges) {
state = AsyncValue.data([...pledges, pledge]);
});
try {
await _savePledge(pledge);
} catch (error) {
// Revert on error
await loadPledges();
rethrow;
}
}
}
class MyWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Local state with hooks
final searchQuery = useState<String>('');
final selectedFilter = useState<PledgeStatus?>(null);
// Controllers
final searchController = useTextEditingController();
final scrollController = useScrollController();
// Effects
useEffect(() {
void onSearchChanged() {
searchQuery.value = searchController.text;
}
searchController.addListener(onSearchChanged);
return () => searchController.removeListener(onSearchChanged);
}, [searchController]);
return /* ... */;
}
}
final animationController = useAnimationController(
duration: Duration(milliseconds: 300),
);
final slideAnimation = useAnimation(
Tween<Offset>(
begin: Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeInOut,
)),
);
final expensiveValue = useMemoized(() {
return performExpensiveComputation(data);
}, [data]);
@riverpod
List<PledgeModel> filteredPledges(FilteredPledgesRef ref) {
final pledges = ref.watch(pledgesProvider).value ?? [];
final searchQuery = ref.watch(searchQueryProvider);
final statusFilter = ref.watch(pledgeStatusFilterProvider);
return pledges.where((pledge) {
final matchesSearch = searchQuery.isEmpty ||
pledge.dareId.contains(searchQuery);
final matchesStatus = statusFilter == null ||
pledge.status == statusFilter;
return matchesSearch && matchesStatus;
}).toList();
}
@riverpod
PledgeService pledgeService(PledgeServiceRef ref) {
final appDb = ref.watch(appDbProvider);
final cache = ref.watch(cacheProvider);
return PledgeService(appDb: appDb, cache: cache);
}
@riverpod
Future<DashboardData> dashboardData(DashboardDataRef ref) async {
final pledges = await ref.watch(pledgesProvider.future);
final user = await ref.watch(currentUserProvider.future);
final stats = await ref.watch(statsProvider.future);
return DashboardData(
pledges: pledges,
user: user,
stats: stats,
);
}
@riverpod
class PaginatedPledges extends _$PaginatedPledges {
@override
Future<PaginatedData<PledgeModel>> build() async {
return _loadPage(1);
}
Future<void> loadNextPage() async {
final current = state.value;
if (current?.hasNextPage != true) return;
state = AsyncValue.loading();
try {
final nextPage = await _loadPage(current!.currentPage + 1);
state = AsyncValue.data(
current.copyWith(
items: [...current.items, ...nextPage.items],
currentPage: nextPage.currentPage,
hasNextPage: nextPage.hasNextPage,
),
);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
}
Future<void> updatePledgeStatus(PledgeModel pledge, PledgeStatus newStatus) async {
// Optimistic update
ref.read(pledgesProvider.notifier).updateLocal(
pledge.copyWith(status: newStatus),
);
try {
await pledgeService.updatePledge(pledge.id!, newStatus);
} catch (error) {
// Revert on error
ref.invalidate(pledgesProvider);
rethrow;
}
}
Future<void> refreshData() async {
// Invalidate specific providers
ref.invalidate(pledgesProvider);
ref.invalidate(userStatsProvider);
// Or invalidate by family parameter
ref.invalidate(pledgesForDareProvider('dare123'));
}
void main() {
testWidgets('pledges display correctly', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
pledgesProvider.overrideWith((ref) async => mockPledges),
],
child: MyApp(),
),
);
expect(find.text('Test Pledge'), findsOneWidget);
});
}
void main() {
testWidgets('full pledge flow', (tester) async {
final container = ProviderContainer();
// Test provider directly
final pledges = await container.read(pledgesProvider.future);
expect(pledges.length, 2);
container.dispose();
});
}
class MyProviderObserver extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('Provider ${provider.name} updated: $newValue');
}
}
void main() {
runApp(
ProviderScope(
observers: [MyProviderObserver()],
child: MyApp(),
),
);
}
// In development mode
final pledgesState = ref.read(pledgesProvider);
print('Current pledges state: $pledgesState');
Use
.select()for partial updates:final pledgeCount = ref.watch(pledgesProvider.select((value) => value.whenData((pledges) => pledges.length) ));Dispose providers when not needed:
ref.invalidate(expensiveProvider);Use
autoDisposefor temporary data:@riverpod Future<Data> temporaryData(TemporaryDataRef ref) async { ref.keepAlive(); // Prevents auto-disposal if needed return fetchData(); }
- Add Riverpod dependencies
- Wrap app with
ProviderScope - Convert StatefulWidgets to HookConsumerWidget
- Create providers for services and state
- Replace manual state management with hooks
- Add error handling with AsyncValue
- Implement optimistic updates
- Add tests for providers
- Set up code generation
- Configure provider observers for debugging
find_pledges_simple.dart- Simple example with basic providersfind_pledges.dart- Complete example with advanced patternspledge_providers.dart- Provider definitions with code generationmain_riverpod_example.dart- App setup and navigation
This migration provides you with a robust, scalable, and maintainable state management solution that handles offline mode, caching, and real-time updates automatically.