Isolates (True Multithreading)
The Problem with Single-Threaded Dart
Dart runs on a single thread. For most tasks — network requests, database queries, file I/O — this is fine because those operations are async and don't block the event loop. But for CPU-intensive work (image processing, JSON parsing of huge payloads, cryptography), a single thread is not enough.
If you compute the Fibonacci of 45 synchronously, you freeze the UI for hundreds of milliseconds. The solution is Isolates.
What are Isolates?
Isolates are Dart's version of threads, with a critical difference: they share no memory. Each isolate has its own heap. Communication happens only via message passing (sending objects that are copied, not shared).
This design eliminates entire categories of concurrency bugs (data races, deadlocks from shared state) at the cost of memory overhead from copying.
Isolate.run — The Modern Simple API
dartimport 'dart:isolate'; // CPU-heavy function — runs in a background isolate int computeFibonacci(int n) { if (n <= 1) return n; return computeFibonacci(n - 1) + computeFibonacci(n - 2); } // In your main isolate (UI thread): Future<void> onButtonPress() async { // This does NOT block the UI final result = await Isolate.run(() => computeFibonacci(45)); print('Fibonacci(45) = $result'); }
Flutter's compute() Helper
Flutter provides a convenience wrapper around Isolate.run:
dartimport 'package:flutter/foundation.dart'; // Parse a large JSON payload without freezing the UI Future<List<Product>> parseProductsInBackground(String jsonString) { return compute(_parseProducts, jsonString); } // This runs in a background isolate — must be a top-level or static function List<Product> _parseProducts(String json) { final decoded = jsonDecode(json) as List; return decoded.map((e) => Product.fromJson(e)).toList(); }
Long-Lived Isolates with ReceivePort
For complex bidirectional communication, use ReceivePort and SendPort:
dartimport 'dart:isolate'; Future<void> spawnWorkerIsolate() async { final receivePort = ReceivePort(); // Spawn the isolate, pass its own SendPort to communicate back await Isolate.spawn(workerIsolate, receivePort.sendPort); // Get the worker's SendPort (first message) final workerSendPort = await receivePort.first as SendPort; // Send work to the isolate final responsePort = ReceivePort(); workerSendPort.send({'task': 'compute', 'reply': responsePort.sendPort, 'n': 40}); final result = await responsePort.first; print('Result from isolate: $result'); receivePort.close(); } // This function runs IN the new isolate void workerIsolate(SendPort mainSendPort) async { final port = ReceivePort(); mainSendPort.send(port.sendPort); // Send our port back to main await for (final message in port) { if (message is Map) { final n = message['n'] as int; final replyPort = message['reply'] as SendPort; replyPort.send(computeFibonacci(n)); } } }
When to Use Isolates
- JSON parsing of payloads > 50KB
- Image decoding and compression
- Cryptographic operations (hashing, encryption)
- Complex mathematical computations
- Any operation that noticeably blocks the UI
Summary
Isolates are Dart's concurrency primitive — true parallel execution without shared memory hazards. For most Flutter apps, Isolate.run() or compute() is all you need. For persistent background workers, the ReceivePort/SendPort API gives full control. The key insight: Dart's "no shared memory" design makes concurrency safe by construction.