Skip to content

Concurrency Patterns

Concurrency Patterns

Standard patterns for handling concurrent operations in mobile applications.

Prevent excessive API calls from rapid user input (e.g., search-as-you-type).

@riverpod
class 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);
}
}
}
  • Search fields with real-time results
  • Filter inputs that trigger API calls
  • Any input that fires rapidly and triggers network requests

Cancel in-flight requests when they become irrelevant (e.g., navigating away, new search replacing old).

@riverpod
class 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));
}
}
@riverpod
class 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();
}

Prevent users from submitting the same action multiple times.

@riverpod
class 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;
}
}
}
ElevatedButton(
onPressed: submitState.isLoading ? null : () => notifier.submit(formData),
child: submitState.isLoading
? const CircularProgressIndicator()
: const Text('Submit'),
)

Discard responses from outdated requests when a newer request has been made.

@riverpod
class 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);
}
}
}
}

Riverpod’s ref.invalidateSelf() naturally handles staleness by disposing the old provider and creating a new one:

@riverpod
class 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
}
}

PatternProblemSolution
DebouncingRapid input triggers excessive API callsDelay execution until input settles
CancellationIn-flight requests become irrelevantCancel via autoDispose or CancelToken
Double-submitUser clicks submit multiple timesGuard with flag, disable UI during submit
Stale responseOld response arrives after newer requestVersion check or invalidateSelf