Answer
Overview
Completer is a Dart class that allows you to manually complete a Future at any time. It's useful when you need to create a Future that doesn't follow the standard async/await pattern.
Basic Concept
A Completer creates a Future that you control.
dartimport 'dart:async'; void main() async { final completer = Completer<String>(); // Get the Future final future = completer.future; // Complete it later Future.delayed(Duration(seconds: 2), () { completer.complete('Hello from Completer!'); }); // Await the Future final result = await future; print(result); // Hello from Completer! }
Creating a Completer
dart// Generic Completer final completer = Completer<int>(); // Complete with a value completer.complete(42); // Complete with an error completer.completeError(Exception('Something went wrong')); // Check if completed if (completer.isCompleted) { print('Already completed'); }
Use Case 1: Bridging Callbacks to Futures
Convert callback-based APIs to Futures.
dartFuture<String> fetchDataWithCallback() { final completer = Completer<String>(); // Simulate callback-based API someAsyncOperation((result, error) { if (error != null) { completer.completeError(error); } else { completer.complete(result); } }); return completer.future; } // Usage final data = await fetchDataWithCallback(); print(data);
Real-world example (Platform Channel):
dartFuture<String> getBatteryLevel() { final completer = Completer<String>(); const platform = MethodChannel('com.example/battery'); platform.invokeMethod('getBatteryLevel').then((result) { completer.complete(result); }).catchError((error) { completer.completeError(error); }); return completer.future; }
Use Case 2: Waiting for User Action
Wait for user to complete an action (button tap, dialog dismiss).
dartFuture<bool> showConfirmationDialog(BuildContext context) { final completer = Completer<bool>(); showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Confirm'), content: Text('Are you sure?'), actions: [ TextButton( onPressed: () { Navigator.pop(context); completer.complete(false); // User cancelled }, child: Text('Cancel'), ), TextButton( onPressed: () { Navigator.pop(context); completer.complete(true); // User confirmed }, child: Text('OK'), ), ], ); }, ); return completer.future; } // Usage final confirmed = await showConfirmationDialog(context); if (confirmed) { print('User confirmed'); } else { print('User cancelled'); }
Use Case 3: Custom Stream Listener
Convert stream events to a single Future.
dartFuture<String> waitForFirstEvent(Stream<String> stream) { final completer = Completer<String>(); final subscription = stream.listen((data) { if (!completer.isCompleted) { completer.complete(data); // Complete on first event } }, onError: (error) { if (!completer.isCompleted) { completer.completeError(error); } }); // Cleanup subscription after completion completer.future.whenComplete(() => subscription.cancel()); return completer.future; } // Usage final firstEvent = await waitForFirstEvent(myStream); print('First event: $firstEvent');
Use Case 4: Timeout Handling
Add custom timeout logic.
dartFuture<String> fetchDataWithTimeout(Duration timeout) { final completer = Completer<String>(); // Start async operation fetchData().then((result) { if (!completer.isCompleted) { completer.complete(result); } }).catchError((error) { if (!completer.isCompleted) { completer.completeError(error); } }); // Timeout timer Timer(timeout, () { if (!completer.isCompleted) { completer.completeError(TimeoutException('Request timed out')); } }); return completer.future; } // Usage try { final data = await fetchDataWithTimeout(Duration(seconds: 5)); print(data); } catch (e) { print('Error: $e'); }
Use Case 5: Coordinating Multiple Async Tasks
Wait for first successful result from multiple sources.
dartFuture<String> fetchFromMultipleSources() { final completer = Completer<String>(); // Try API 1 fetchFromAPI1().then((result) { if (!completer.isCompleted) { completer.complete(result); } }).catchError((_) {}); // Try API 2 fetchFromAPI2().then((result) { if (!completer.isCompleted) { completer.complete(result); } }).catchError((_) {}); // Try API 3 fetchFromAPI3().then((result) { if (!completer.isCompleted) { completer.complete(result); } }).catchError((_) {}); return completer.future; // Returns first successful result }
Complete Example: Custom Loading Manager
dartclass LoadingManager { Completer<void>? _completer; Future<void> showLoading() async { _completer = Completer<void>(); return _completer!.future; } void hideLoading() { if (_completer != null && !_completer!.isCompleted) { _completer!.complete(); } } } // Usage final loadingManager = LoadingManager(); void loadData() async { // Show loading and wait until it's dismissed final loadingFuture = loadingManager.showLoading(); // Simulate data fetch await Future.delayed(Duration(seconds: 3)); // Hide loading loadingManager.hideLoading(); // Wait for loading to be dismissed await loadingFuture; print('Data loaded'); }
Completer vs Future
| Feature | Completer | Future |
|---|---|---|
| Creation | Manual (you control when it completes) | Automatic (completes on its own) |
| Use case | Callbacks, custom async logic | Standard async operations |
| Control | Full control over completion | No control |
dart// Future — completes automatically Future<String> fetchData() async { return 'Data'; // Completes when function returns } // Completer — completes manually Future<String> fetchDataWithCompleter() { final completer = Completer<String>(); // Complete whenever you want someCallback(() { completer.complete('Data'); }); return completer.future; }
Common Pitfalls
1. Completing Twice
dartfinal completer = Completer<String>(); completer.complete('First'); completer.complete('Second'); // ❌ StateError: Already completed // ✅ Check before completing if (!completer.isCompleted) { completer.complete('Value'); }
2. Not Handling Errors
dartfinal completer = Completer<String>(); // ❌ Unhandled error (if never completed) // completer.future.then(...); // ✅ Handle errors completer.future.then((value) { print(value); }).catchError((error) { print('Error: $error'); });
3. Memory Leaks
dart// ❌ Completer never completed → Future never resolves final completer = Completer<String>(); return completer.future; // ❌ Hangs forever // ✅ Always complete or add timeout Timer(Duration(seconds: 5), () { if (!completer.isCompleted) { completer.completeError(TimeoutException('Timeout')); } });
Best Practices
dart// ✅ Check if already completed if (!completer.isCompleted) { completer.complete(value); } // ✅ Use completeError for errors if (hasError) { completer.completeError(Exception('Error')); } // ✅ Add timeout to prevent hanging Futures Timer(timeout, () { if (!completer.isCompleted) { completer.completeError(TimeoutException('Timeout')); } }); // ✅ Cleanup resources when Future completes completer.future.whenComplete(() { // Cleanup subscription.cancel(); });
Summary
| Use Case | Example |
|---|---|
| Callbacks → Futures | Platform channels, legacy APIs |
| User actions | Wait for dialog dismiss, button tap |
| Stream → Future | Get first event from stream |
| Custom timeout | Add timeout logic |
| Multiple sources | Return first successful result |
When to use:
- ✅ Converting callbacks to Futures
- ✅ Waiting for user actions
- ✅ Custom async control flow
- ❌ Standard async operations (use )text
async/await
Learn more: Completer Documentation