Riverpod Leak Detection & Prevention
Memory management is a critical aspect of mobile development. Fasq Riverpod is designed to be “leak-proof” by default, leveraging Riverpod’s built-in disposal mechanisms.
Automatic Disposal
Section titled “Automatic Disposal”The standard queryProvider uses Riverpod’s AutoDisposeAsyncNotifier. This means that as soon as the last widget unmounts (or the last listener is removed), the provider is marked for disposal.
How it Works
Section titled “How it Works”When a provider is disposed:
- Riverpod calls the notifier’s
_cleanupmethod. fasq_riverpodcancels all internal stream subscriptions.fasq_riverpodcallsquery.removeListener().- If no other providers are using that query, the core
Queryenters a “stale” state and is eventually removed from memory after itscacheTimeexpires.
Manual Disposal in Custom Providers
Section titled “Manual Disposal in Custom Providers”If you are creating custom providers that interact with the QueryClient directly (via fasqClientProvider), it is your responsibility to ensure resources are cleaned up.
Always use ref.onDispose to remove listeners:
final customProvider = Provider((ref) { final client = ref.watch(fasqClientProvider); final query = client.getQuery(...);
query.addListener();
// CRITICAL: Ensure the listener is removed when the provider is disposed ref.onDispose(() { query.removeListener(); });
return query;});Testing for Leaks
Section titled “Testing for Leaks”Fasq includes a LeakDetector utility specifically for identifying memory leaks in your unit and widget tests.
Integration with Riverpod Tests
Section titled “Integration with Riverpod Tests”When testing providers, you should verify that no queries are left active after the ProviderContainer is disposed.
import 'package:fasq/testing.dart';import 'package:fasq_riverpod/fasq_riverpod.dart';import 'package:flutter_test/flutter_test.dart';
void main() { late LeakDetector detector;
setUp(() { detector = LeakDetector(); });
test('provider should not leak queries', () async { final container = ProviderContainer(); final client = container.read(fasqClientProvider);
// Use a query provider final result = await container.read(myQueryProvider.future);
// Dispose the container (this should trigger provider disposal) container.dispose();
// Verify no queries are leaked detector.expectNoLeakedQueries(client); });}Debug Instrumentation
Section titled “Debug Instrumentation”In debug mode only, you can inspect the reference holders of any query to see exactly what is keeping it alive. This is useful for identifying “zombie” providers or widgets.
final client = ref.read(fasqClientProvider);final debugInfo = client.activeQueryDebugInfoMap;
for (final entry in debugInfo.entries) { print('Query ${entry.key} is held by: ${entry.value.referenceHolders.keys}');}Best Practices
Section titled “Best Practices”- Use AutoDispose: Always prefer
AutoDisposeproviders unless you have a specific reason to keep data in memory indefinitely. - Check Tests: Run your tests with
LeakDetectorto catch leaks early in the development cycle. - Avoid Global Clients: Avoid creating
QueryClientinstances outside offasqClientProvider, as they won’t benefit from Riverpod’s lifecycle management.