Managing Dart Concurrency and Isolates
Contents
Core Concepts
Decision Matrix: Async vs. Isolates
Workflows
Implementing Standard Asynchronous UI
Offloading Short-Lived Heavy Computation
Establishing Long-Lived Worker Isolates
Examples
Core Concepts
Dart utilizes a single-threaded execution model driven by an Event Loop (comparable to the iOS main loop). By default, all Flutter application code runs on the Main Isolate.
Asynchronous Operations (
async
/
await
):
Use for non-blocking I/O tasks (network requests, file access). The Event Loop continues processing other events while waiting for the
Future
to complete.
Isolates:
Dart's implementation of lightweight threads. Isolates possess their own isolated memory and do not share state. They communicate exclusively via message passing.
Main Isolate:
The default thread where UI rendering and event handling occur. Blocking this isolate causes UI freezing (jank).
Worker Isolate:
A spawned isolate used to offload CPU-bound tasks (e.g., decoding large JSON blobs) to prevent Main Isolate blockage.
Decision Matrix: Async vs. Isolates
Apply the following conditional logic to determine the correct concurrency approach:
If
the task is I/O bound (e.g., HTTP request, database read) ->
Use
async
/
await
on the Main Isolate.
If
the task is CPU-bound but executes quickly (< 16ms) ->
Use
async
/
await
on the Main Isolate.
If
the task is CPU-bound, takes significant time, and runs once (e.g., parsing a massive JSON payload) ->
Use
Isolate.run()
.
If
the task requires continuous or repeated background processing with multiple messages passed over time ->
Use
Isolate.spawn()
with
ReceivePort
and
SendPort
.
Workflows
Implementing Standard Asynchronous UI
Use this workflow to fetch and display non-blocking asynchronous data.
Task Progress:
Mark the data-fetching function with the
async
keyword.
Return a
Future
fetchUserData ( ) async { await Future . delayed ( const Duration ( seconds : 2 ) ) ; // Simulate network I/O return "User Data Loaded" ; } // 2. Consume in the UI Widget build ( BuildContext context ) { return FutureBuilder < String
( future : fetchUserData ( ) , builder : ( context , snapshot ) { if ( snapshot . connectionState == ConnectionState . waiting ) { return const CircularProgressIndicator ( ) ; } else if ( snapshot . hasError ) { return Text ( 'Error: ${ snapshot . error } ' ) ; } else { return Text ( 'Result: ${ snapshot . data } ' ) ; } } , ) ; } Example 2: Short-Lived Isolate ( Isolate.run ) import 'dart:isolate' ; import 'dart:convert' ; // 1. Define the heavy computation callback // Note: Adhering to the strict single-argument signature requirement. List < dynamic
decodeHeavyJson ( String jsonString ) { return jsonDecode ( jsonString ) as List < dynamic
; } // 2. Offload to a worker isolate Future < List < dynamic
processDataInBackground ( String rawJson ) async { // Isolate.run spawns the isolate, runs the computation, returns the value, and exits. final result = await Isolate . run ( ( ) =
decodeHeavyJson ( rawJson ) ) ; return result ; } Example 3: Long-Lived Isolate ( ReceivePort / SendPort ) import 'dart:isolate' ; class WorkerManager { late SendPort _workerSendPort ; final ReceivePort _mainReceivePort = ReceivePort ( ) ; Isolate ? _isolate ; Future < void
initialize ( ) async { // 1. Spawn isolate and pass the Main Isolate's SendPort _isolate = await Isolate . spawn ( _workerEntry , _mainReceivePort . sendPort ) ; // 2. Listen for messages from the Worker Isolate _mainReceivePort . listen ( ( message ) { if ( message is SendPort ) { // First message is the Worker's SendPort _workerSendPort = message ; _startCommunication ( ) ; } else { // Subsequent messages are data payloads print ( 'Main Isolate received: $ message ' ) ; } } ) ; } void _startCommunication ( ) { // Send data to the worker _workerSendPort . send ( "Process this data" ) ; } // 3. Worker Isolate Entry Point static void _workerEntry ( SendPort mainSendPort ) { final workerReceivePort = ReceivePort ( ) ; // Send the Worker's SendPort back to the Main Isolate mainSendPort . send ( workerReceivePort . sendPort ) ; // Listen for incoming tasks workerReceivePort . listen ( ( message ) { print ( 'Worker Isolate received: $ message ' ) ; // Perform work and send result back final result = "Processed: $ message " ; mainSendPort . send ( result ) ; } ) ; } void dispose ( ) { _mainReceivePort . close ( ) ; _isolate ? . kill ( ) ; } }