isolate_runner_mixin
A Flutter mixin for running work off the UI isolate.
It gives you two APIs:
runInIsolate/IsolateRunnerMixin.run: one-off work in a background isolatespawnWorker+requestWorker: long-lived persistent worker isolate
Quick start
- Use
runInIsolate(or the staticIsolateRunnerMixin.run) for a single background call. - Use
spawnWorker+requestWorkerfor many calls over time (e.g. repeated heavy computation in a service).
One-off (static — no class needed):
import 'package:isolate_runner_mixin/isolate_runner_mixin.dart';
int _sum(int n) {
var total = 0;
for (var i = 0; i < n; i++) total += i;
return total;
}
// Call directly, anywhere — no class or mixin instance required.
final result = await IsolateRunnerMixin.run(() => _sum(50000000));
One-off (via mixin instance):
class MyService with IsolateRunnerMixin {
Future<int> compute(int n) {
return runInIsolate(() => _sum(n));
}
}
Installation
dependencies:
isolate_runner_mixin: <latest_version>
Run flutter pub get.
Usage: One-Off Tasks
Static API
Use IsolateRunnerMixin.run when you don't want to (or can't) add the mixin to
a class — useful in utility functions, test helpers, or top-level code:
import 'package:isolate_runner_mixin/isolate_runner_mixin.dart';
final result = await IsolateRunnerMixin.run(
() => expensiveComputation(data),
mode: IsolateRunMode.alwaysIsolate,
timeout: const Duration(seconds: 5),
);
Pass a single typed argument with IsolateRunnerMixin.runWithArg:
final length = await IsolateRunnerMixin.runWithArg<String, int>(
myString,
(s) => s.length * 42,
);
Instance API
Mix into any class and call runInIsolate:
import 'package:isolate_runner_mixin/isolate_runner_mixin.dart';
bool _verify(String data, String sig, String pubKey) {
// ... verification logic
return true;
}
class MyService with IsolateRunnerMixin {
final String publicKey;
MyService(this.publicKey);
Future<bool> verify(String data, String signature) {
return runInIsolate(
() => _verify(data, signature, publicKey),
mode: IsolateRunMode.alwaysIsolate,
);
}
}
Pass a single argument with runInIsolateWithArg:
Future<int> doubled(int value) {
return runInIsolateWithArg(value, (v) => v * 2);
}
What the callback receives
You pass a closure. The closure body runs in the target isolate — not the call site:
// ✗ Wrong: _sum(n) runs on the main isolate right now.
final result = _sum(n);
await runInIsolate(() => result);
// ✓ Correct: _sum(n) runs inside the background isolate.
await runInIsolate(() => _sum(n));
Modes
| Mode | Behaviour |
|---|---|
IsolateRunMode.auto |
Background isolate when a RootIsolateToken is available, otherwise falls back to the current isolate. Default. |
IsolateRunMode.alwaysIsolate |
Always spawns a background isolate. |
IsolateRunMode.currentIsolate |
Always runs on the current isolate (useful for testing). |
Usage: Persistent Worker
Use a persistent worker when you need many requests over time. Requests are processed sequentially — the worker handles one command at a time in the order they are received.
The handler must be a top-level or static function:
import 'dart:async';
import 'package:isolate_runner_mixin/isolate_runner_mixin.dart';
FutureOr<Object?> workerHandler(String command, Object? payload) async {
switch (command) {
case 'double':
return (payload as int) * 2;
case 'delayEcho':
final map = payload as Map<Object?, Object?>;
await Future<void>.delayed(Duration(milliseconds: map['delayMs'] as int));
return map['value'];
}
throw UnsupportedError('Unknown command: $command');
}
Initialize once, then send requests:
class MyService with IsolateRunnerMixin {
Future<void> init() async {
await spawnWorker(
handler: workerHandler,
options: const SpawnWorkerOptions(
maxPendingRequests: 500, // queue overflow protection
startupTimeout: Duration(seconds: 10), // optional startup deadline
),
);
}
Future<int> doubleValue(int input) {
return requestWorker<int>(
command: 'double',
payload: input,
timeout: const Duration(seconds: 5), // optional per-request deadline
);
}
bool get workerAlive => isWorkerRunning;
Future<void> dispose() async {
await disposeWorker();
}
}
Worker lifecycle
spawnWorkeris idempotent — safe to call multiple times while running.- If startup is already in progress, other
spawnWorkercalls await the same startup future. - Calling
spawnWorkeragain with a differenthandleroroptionsrestarts the worker with the new configuration. requestWorkerauto-respawns the worker after a dispose if a handler was previously registered.isWorkerRunningreturnstruewhen the worker isolate is alive.disposeWorkermust be called when the owner lifecycle ends. Pending requests are failed withIsolateWorkerDisposedException.
Payload contract
requestWorker payloads and worker results must be one of:
null,bool,num,StringList/Map(recursively containing supported types — no circular references)TransferableTypedDataSendPort
Custom classes are not accepted directly — convert to Map / List first.
Circular references are detected and rejected before sending.
Error handling
try {
final result = await service.requestWorker<int>(command: 'double', payload: 5);
} on IsolateWorkerRemoteException catch (e) {
// The handler threw inside the worker isolate.
print('Command "${e.command}" failed: ${e.remoteError}');
print(e.remoteStackTrace);
} on IsolateWorkerDisposedException catch (_) {
// disposeWorker() was called while this request was pending.
} on IsolateWorkerQueueOverflowException catch (_) {
// Too many pending requests — exceeded maxPendingRequests.
} on IsolateWorkerTerminatedException catch (_) {
// The worker isolate crashed unexpectedly.
} on TimeoutException catch (_) {
// Per-request timeout elapsed.
}
All exceptions extend IsolateWorkerException, so you can catch the base type
for a broad handler:
} on IsolateWorkerException catch (e) {
print('Worker error: $e');
}
Flutter Lifecycle Example
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:isolate_runner_mixin/isolate_runner_mixin.dart';
FutureOr<Object?> _workerHandler(String command, Object? payload) async {
// ... handle commands
}
class _MyState extends State<MyWidget> with IsolateRunnerMixin {
@override
void initState() {
super.initState();
spawnWorker(handler: _workerHandler);
}
@override
void dispose() {
unawaited(disposeWorker());
super.dispose();
}
}
In debug mode, the package prints a warning if disposeWorker is missed.
Rules for reliable usage
- Keep heavy work inside the callback body — work done before the callback runs on the main isolate.
- Prefer top-level or
staticfunctions for isolate/worker code. - Send only supported payload/result types (see payload contract above).
- No circular references in payloads — they are rejected before sending.
- If you use
spawnWorker, calldisposeWorker()in your owner lifecycle.
Common mistakes
// ✗ Wrong: heavyCalc runs on the main isolate before runInIsolate is called.
final value = heavyCalc();
await runInIsolate(() => value);
// ✓ Correct: heavyCalc runs in the target isolate.
await runInIsolate(() => heavyCalc());
// ✗ Wrong: custom class is not directly sendable as request payload.
await requestWorker(command: 'save', payload: MyModel(...));
// ✓ Correct: convert to map/list first.
await requestWorker(command: 'save', payload: myModel.toJson());
// ✗ Wrong: circular reference is rejected (not sendable across isolates).
final map = <Object?, Object?>{};
map['self'] = map;
await requestWorker(command: 'echo', payload: map);
Example App
For a complete app demonstrating all three APIs (static, instance, and persistent
worker), see the example/ directory.