In Dart, all code runs in isolates, which are kinda like threads, but with their own private memory. They don't share info directly and only talk by sending messages. By default, Flutter apps do everything in one isolate - the main one. This setup usually keeps things simple and your app's UI responsive.
But sometimes, if you're doing huge computations, your app might get "UI jank" or that annoying jerky motion. If that's happening, you can move those big calculations to another isolate. This lets your app handle those heavy tasks alongside everything else, using multiple cores if your device has them.
Each isolate has its own memory and event loop. The event loop just processes stuff in the order it's added to a queue. On the main isolate, this could be things like handling a tap on the screen, running functions, or drawing stuff on the screen. Imagine a queue with a few events waiting to be handled.
Whenever a process can't be completed in a frame gap, the time between two frames, it's a good idea to offload the work to another isolate to ensure that the main isolate can produce 60 frames per second. When you spawn an isolate in Dart, it can process the work concurrently with the main isolate, without blocking it.

Message passing between isolates
Dart isolates follow the Actor model, meaning they communicate solely through message passing using Port objects. When a message is sent from one isolate to another, it's usually copied from the sender to the receiver. This ensures that any changes made to the message in one isolate won't affect the original.
However, immutable objects like Strings or unmodifiable bytes are an exception. Instead of copying, a reference to them is sent across the port for better performance. Since these objects can't be changed, this maintains the actor model's behavior.
There's a special case when an isolate exits after sending a message using Isolate.exit. In this scenario, ownership of the message is passed from one isolate to another, ensuring only one isolate can access it.
For sending messages, the two main methods are SendPort.send, which copies mutable messages as it sends them, and Isolate.exit, which sends a reference to the message. Both Isolate.run and compute internally use Isolate.exit.
Short-lived isolates
To easily move a process to an isolate in Flutter, you can use the Isolate.run method. Here's how it works: it creates a new isolate, sends a callback function to start a computation, retrieves the result, and then closes the isolate. And the best part? It all happens alongside the main isolate, without slowing it down.
When using Isolate.run, you just need to provide a callback function as its argument. This function should take one required, unnamed argument. Once the computation is done, the result is sent back to the main isolate, and the spawned isolate exits.
ElevatedButton(
onPressed: () {
Isolate.run(() => heavyTask(type: "1"));
},
heavyTask({String? type}) {
int k = 108;
for (int i = 1; i < 999999999; i++)k+=i;
print(type ?? "jk");
print(k);
}
Stateful, longer-lived isolates
Short-lived isolates are convenient but have performance overhead due to spawning new isolates and copying objects between them. If you find yourself repeatedly using Isolate.run for the same computation, you might get better performance by creating isolates that stick around for a while.
To achieve this, you can use lower-level isolate APIs like
* Isolate.spawn() and
* Isolate.exit(), along with
* ReceivePort and SendPort.
When you use Isolate.run, the new isolate closes down right after sending a single message to the main isolate. But sometimes, you need isolates that hang around longer and can exchange multiple messages over time. In Dart, these are called background workers.
Background workers are handy when you have tasks that need to run repeatedly throughout your app's lifetime or when a task runs for a while and sends back multiple results to the main isolate.
ReceivePorts and SendPorts
Using platform plugins in isolates
Additionally, starting from Flutter 3.7, you can use platform plugins in background isolates. This allows you to offload heavy, platform-specific tasks to an isolate without freezing your UI. For instance, encrypting data using a native API can now be done in a background isolate, improving overall performance.
Platform channel isolates use the BackgroundIsolateBinaryMessenger API. Check out the Dart documentation for detailed examples on setting up two-way communication between the main isolate and a worker isolate.
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
// Identify the root isolate to pass to the background isolate.
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
Isolate.spawn(_isolateMain, rootIsolateToken);
}
Future<void> _isolateMain(RootIsolateToken rootIsolateToken) async {
// Register the background isolate with the root isolate.
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
// You can now use the shared_preferences plugin.
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
print(sharedPreferences.getBool('isDebug'));
}
Limitations of Isolates
If you're familiar with multithreading from other languages, you might expect isolates in Dart to behave like threads. However, isolates have their own unique behavior. They maintain separate global fields and can only communicate through message passing. This means that mutable objects within an isolate are isolated and inaccessible to other isolates. For instance, if your app has a global mutable variable like "configuration", it's copied as a new global field in a spawned isolate. Any changes made to this variable in the spawned isolate won't affect the original in the main isolate, even if passed as a message. It's important to understand this behavior when working with isolates.
Web Platforms and Compute
Dart web platforms, including Flutter web, do not support isolates. To ensure your code compiles for the web, you can use the compute method. On the web, compute() runs the computation on the main thread, while on mobile devices, it spawns a new thread. So, await compute(fun, message) is equivalent to await Isolate.run(() => fun(message)) on mobile and desktop platforms.
0 Comments