locked_async 0.1.2 copy "locked_async: ^0.1.2" to clipboard
locked_async: ^0.1.2 copied to clipboard

A package for locking asynchronous operations.

Getting Started #

Install the package with:

dart pub add locked_async

Import and create a lock:

import 'package:locked_async/locked_async.dart';

final lock = LockedAsync();

await lock.run((state) async {
  final data = await fetchData();
  state.guard();
  processData(data);  // Won't run if another task started
});

That's it. The lock ensures only one task runs at a time, automatically canceling old ones when new ones start.

Why Do We Need This? #

Let's say you've got a search input where every keystroke triggers an API call. Simple, right?

final results = ValueNotifier<List<String>>([]);

// The naive approach
Future<void> getSearchResults(String query) async {
  final response = await http.get(Uri.parse('https://api.example.com/search?q=$query'));
  results.value = jsonDecode(response.body);  // But for *which* query?
}

// User types: h-e-l-l-o
getSearchResults('he');    // Request #1, 500ms response time
getSearchResults('hel');   // Request #2, 200ms response time (fast server response)

Here's the problem: Request #2 finishes first! Your UI shows "hel" results, then immediately flips back to "he" results when the slower request completes. Users see wrong results flickering. It's wasteful and confusing.

The LockedAsync Solution #

LockedAsync ensures only one task runs at a time. When a new lock.run() starts, it cancels the previous one. Inside your callback, you get a state object to cooperate with cancellation:

import 'package:locked_async/locked_async.dart';
import 'package:http/http.dart' as http;

final lock = LockedAsync();
final results = ValueNotifier<List<String>>([]);

Future<void> getSearchResults(String query) async {
  await lock.run((state) async {
    // Wrap async operations with state.wait() to check for cancellation
    final response = await http.get(Uri.parse('https://api.example.com/search?q=$query'));

    state.guard(); // Crash the current task if it is cancelled

    results.value = jsonDecode(response.body);
  });
}

Now when users type fast:

  • 'he' starts
  • 'hel' cancels it and starts fresh
  • Only the latest query's results make it to your UI

The Cancellation Catch #

Dart futures don't cancel themselves—you need to cooperate with cancellation. This means checking the cancellation state regularly:

await lock.run((state) async {
  await someSetup();
  state.guard();  // Check if cancelled
  
  await heavyAsyncOperation();
  state.guard();  // Check again - crucial!
  
  // If you forget this, cancelled tasks could still run expensive operations
  processResults();
});

To make this easier, use state.wait() as a wrapper:

await lock.run((state) async {
  await state.wait(() => someSetup());           // Auto-checks cancellation
  await state.wait(() => heavyAsyncOperation()); // Auto-checks again
  processResults();  // Safe outside async ops
});

This will call state.guard() before allowing the task to continue.

The Real Problem: HTTP Doesn't Stop #

Even with the lock, if you're using http, cancelled requests still complete—they just don't update your UI. The network call wastes time and bandwidth. For search-as-you-type, this means waiting seconds longer than necessary.

Solution A: Use Dio with CancelTokens #

Dio actually supports true request cancellation:

import 'package:dio/dio.dart';

final dio = Dio();
final lock = LockedAsync();

Future<void> getSearchResults(String query) async {
  await lock.run((state) async {
    final cancelToken = CancelToken();
    
    // When this task gets cancelled, kill the HTTP request too
    state.onCancel(() => cancelToken.cancel());
    
    final response = await state.wait(() => 
      dio.get('https://api.example.com/search?q=$query', 
              cancelToken: cancelToken)
    );
    
    results.value = jsonDecode(response.data);
  });
}

Now when users type "hel", the "he" request gets aborted immediately. No waiting for slow servers!

Solution B: Add Debouncing #

Even with cancellation, rapid typing can spam your API. Debouncing waits for users to pause typing:

import 'package:dio/dio.dart';

final dio = Dio();
final lock = LockedAsync();

Future<void> getSearchResults(String query) async {
  await lock.run((state) async {
    // Wait 300ms for the user to stop typing
    await state.wait(() => Future.delayed(const Duration(milliseconds: 300)));
    
    // This request won't even start if the task is cancelled
    final response = await state.wait(() => dio.get('https://api.example.com/search?q=$query'));
    
    results.value = jsonDecode(response.data);
  });
}

Combine with the lock and you get instant response when users pause, without the race condition mess.

The Ultimate: Both Together #

import 'package:dio/dio.dart';

final dio = Dio();
final lock = LockedAsync();

Future<void> getSearchResults(String query) async {
  await lock.run((state) async {
    // Wait 300ms for the user to stop typing
    await state.wait(() => Future.delayed(const Duration(milliseconds: 300)));
    
    // Kill the request if the task is cancelled
    final cancelToken = CancelToken();
    state.onCancel(() => cancelToken.cancel());
    
    final response = await state.wait(() => dio.get('https://api.example.com/search?q=$query', cancelToken: cancelToken));
    
    results.value = jsonDecode(response.data);
  });
}

Wait 300ms for the user to stop typing, then make a single cancellable request. Any subsequent requests will cancel the previous one.

Real Talk #

I used to think async was straightforward. Then I built search features that made users rage-quit. This pattern (inspired by Riverpod's ref.onDispose()) solved it for me.

4
likes
140
points
88
downloads

Publisher

verified publisherdickersystems.com

Weekly Downloads

A package for locking asynchronous operations.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

synchronized

More

Packages that depend on locked_async