Concurrency Patterns
Concurrency Patterns
Standard patterns for handling concurrent operations in mobile applications.
Concurrency Patterns
Section titled âConcurrency PatternsâDebouncing
Section titled âDebouncingâPrevent excessive API calls from rapid user input (e.g., search-as-you-type).
Pattern
Section titled âPatternâ@riverpodclass SearchNotifier extends _$SearchNotifier { Timer? _debounceTimer;
@override AsyncValue<List<SearchResult>> build() => const AsyncData([]);
void search(String query) { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 300), () { _executeSearch(query); }); ref.onDispose(() => _debounceTimer?.cancel()); }
Future<void> _executeSearch(String query) async { if (query.isEmpty) { state = const AsyncData([]); return; } state = const AsyncLoading(); final repository = ref.read(searchRepositoryProvider); try { final results = await repository.search(query: query).first; state = AsyncData(results); } catch (e, st) { state = AsyncError(e, st); } }}When to Use
Section titled âWhen to Useâ- Search fields with real-time results
- Filter inputs that trigger API calls
- Any input that fires rapidly and triggers network requests
Request Cancellation
Section titled âRequest CancellationâCancel in-flight requests when they become irrelevant (e.g., navigating away, new search replacing old).
Pattern with Riverpod
Section titled âPattern with Riverpodâ@riverpodclass DetailNotifier extends _$DetailNotifier { @override Stream<DetailModel> build(String id) { // Riverpod's autoDispose handles cancellation automatically: // When the provider is no longer watched, the stream subscription is cancelled. final repository = ref.watch(detailRepositoryProvider); return repository.queryItem(filter: DetailFilter(id: id)); }}Pattern with CancelToken (REST)
Section titled âPattern with CancelToken (REST)â@riverpodclass FileUploadNotifier extends _$FileUploadNotifier { CancelToken? _cancelToken;
@override AsyncValue<UploadResult> build() => const AsyncData(UploadResult.idle);
Future<void> upload(File file) async { _cancelToken?.cancel(); _cancelToken = CancelToken(); state = const AsyncLoading(); try { final result = await ref.read(uploadServiceProvider) .upload(file, cancelToken: _cancelToken!); state = AsyncData(result); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; // Silently ignore state = AsyncError(e, e.stackTrace ?? StackTrace.current); } }
void cancel() => _cancelToken?.cancel();}Double-Submit Prevention
Section titled âDouble-Submit PreventionâPrevent users from submitting the same action multiple times.
Pattern
Section titled âPatternâ@riverpodclass SubmitNotifier extends _$SubmitNotifier { bool _isSubmitting = false;
@override AsyncValue<void> build() => const AsyncData(null);
Future<void> submit(FormData data) async { if (_isSubmitting) return; // Prevent double submit _isSubmitting = true; state = const AsyncLoading(); try { await ref.read(repositoryProvider).create(data); state = const AsyncData(null); } catch (e, st) { state = AsyncError(e, st); } finally { _isSubmitting = false; } }}UI-Level Prevention
Section titled âUI-Level PreventionâElevatedButton( onPressed: submitState.isLoading ? null : () => notifier.submit(formData), child: submitState.isLoading ? const CircularProgressIndicator() : const Text('Submit'),)Stale Response Handling
Section titled âStale Response HandlingâDiscard responses from outdated requests when a newer request has been made.
Pattern with Request Versioning
Section titled âPattern with Request Versioningâ@riverpodclass VersionedSearchNotifier extends _$VersionedSearchNotifier { int _requestVersion = 0;
@override AsyncValue<List<SearchResult>> build() => const AsyncData([]);
Future<void> search(String query) async { final version = ++_requestVersion; state = const AsyncLoading();
try { final results = await ref.read(searchRepositoryProvider) .search(query: query).first;
// Only update state if this is still the latest request if (version == _requestVersion) { state = AsyncData(results); } } catch (e, st) { if (version == _requestVersion) { state = AsyncError(e, st); } } }}Pattern with Riverpod autoDispose
Section titled âPattern with Riverpod autoDisposeâRiverpodâs ref.invalidateSelf() naturally handles staleness by disposing the old provider and creating a new one:
@riverpodclass FilteredListNotifier extends _$FilteredListNotifier { String _currentFilter = '';
@override Stream<List<ItemModel>> build() { return ref.watch(repositoryProvider).queryList( filter: ItemFilter(search: _currentFilter), ); }
void updateFilter(String filter) { _currentFilter = filter; ref.invalidateSelf(); // Old stream disposed, new stream created }}Summary
Section titled âSummaryâ| Pattern | Problem | Solution |
|---|---|---|
| Debouncing | Rapid input triggers excessive API calls | Delay execution until input settles |
| Cancellation | In-flight requests become irrelevant | Cancel via autoDispose or CancelToken |
| Double-submit | User clicks submit multiple times | Guard with flag, disable UI during submit |
| Stale response | Old response arrives after newer request | Version check or invalidateSelf |