Dart Async Programming: Future, Stream, and Isolate — A Complete Guide
Freezes and jank in Flutter apps usually trace back to misunderstanding async. Three concepts, one clear guide.
Why Async Matters
Blocking the UI thread (main isolate) means:
→ Frames miss the 16ms deadline
→ Jank / dropped frames
→ ANR / UI freeze
Solutions:
Future → one-shot async operation (API calls, DB queries)
Stream → continuous data flow (Supabase Realtime, sensors)
Isolate → offload CPU-heavy work to a background thread
Future: One-Shot Async Operations
// Basic form
Future<String> fetchUserName(String userId) async {
final response = await supabase
.from('users')
.select('name')
.eq('id', userId)
.single();
return response['name'] as String;
}
// Error handling
Future<String?> fetchUserNameSafe(String userId) async {
try {
return await fetchUserName(userId);
} on PostgrestException catch (e) {
debugPrint('DB error: ${e.message}');
return null;
}
}
// Parallel execution (faster than sequential)
final results = await Future.wait([
fetchUserName(userId),
fetchUserPlan(userId),
fetchUserStats(userId),
]);
Common mistake:
// ❌ Returns a Future but never awaits it
void loadData() {
fetchUserName(userId); // result ignored
}
// ✅ Await it
Future<void> loadData() async {
final name = await fetchUserName(userId);
setState(() => _name = name);
}
Stream: Continuous Data Flow
// Supabase Realtime as a Stream
class _ChatPageState extends State<ChatPage> {
late final Stream<List<Map<String, dynamic>>> _messagesStream;
@override
void initState() {
super.initState();
_messagesStream = supabase
.from('messages')
.stream(primaryKey: ['id'])
.eq('room_id', widget.roomId)
.order('created_at');
}
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _messagesStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (_, i) => MessageTile(message: snapshot.data![i]),
);
},
);
}
}
Custom Stream with StreamController:
class KpiService {
final _controller = StreamController<KpiData>.broadcast();
Stream<KpiData> get stream => _controller.stream;
void update(KpiData data) => _controller.add(data);
void dispose() => _controller.close();
}
Isolate: Offload CPU-Heavy Work
// compute() — Flutter's easy Isolate wrapper
Future<List<ProcessedItem>> processLargeDataset(
List<RawItem> rawItems,
) async {
return compute(_processItems, rawItems);
}
// Must be a top-level function (Isolate constraint)
List<ProcessedItem> _processItems(List<RawItem> items) {
return items.map((item) => ProcessedItem(
id: item.id,
hash: sha256.convert(utf8.encode(item.data)).toString(),
)).toList();
}
Long-running background worker with Isolate.spawn:
Future<void> startBackgroundWorker() async {
final receivePort = ReceivePort();
await Isolate.spawn(_workerEntryPoint, receivePort.sendPort);
await for (final message in receivePort) {
if (message is WorkerResult) _handleResult(message);
}
}
void _workerEntryPoint(SendPort sendPort) {
while (true) {
// heavy work...
sendPort.send(WorkerResult(data: 'result'));
}
}
When to Use What
Future → API / DB / file I/O (completes once)
Stream → Realtime / WebSocket / sensors (arrives continuously)
Isolate → heavy computation / bulk data / encryption (pegs the CPU)
Summary
Rules:
1. Never block the UI thread — use async/await
2. Parallelize independent work with Future.wait
3. Realtime data = Stream + StreamBuilder
4. CPU-heavy work = compute() → Isolate
5. Always handle errors with try/catch
Most Flutter "slowness" is fixable with compute(). Start there.
Top comments (0)