running-flutter-isolates — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited running-flutter-isolates (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Move heavy computations off the main isolate to prevent UI jank (>16ms frame gaps), using the simplest API that fits the use case.
Use compute() when: | Use Isolate.spawn() when: |
|---|---|
| ✅ One-off computation | ✅ Long-lived worker needed |
| ✅ Simple serializable data | ✅ Multiple messages over time |
| ✅ Auto-cleanup desired | ✅ Custom messaging protocol |
| ✅ Web compatibility required | ✅ Avoid repeated spawn overhead |
| ❌ NOT for long-running tasks | ❌ NOT for single one-shot tasks |
| Additional Scenarios | API |
|---|---|
| One-shot, single return value (Dart 2.19+) | Isolate.run() |
| Need platform plugins in background | BackgroundIsolateBinaryMessenger |
Isolate.run()Best for single, one-off computations. The isolate spawns, runs the task, returns the value, then shuts down automatically.
// Decode a large JSON file without blocking the UI thread
Future<List<Photo>> getPhotos() async {
// Load asset on the main isolate first (rootBundle not accessible in isolate)
final String jsonString = await rootBundle.loadString('assets/photos.json');
// Offload CPU-heavy decoding to a new isolate
final List<Photo> photos = await Isolate.run<List<Photo>>(() {
final List<Object?> photoData = jsonDecode(jsonString) as List<Object?>;
return photoData.cast<Map<String, Object?>>().map(Photo.fromJson).toList();
});
return photos;
}Key behavior: The result is transferred (not copied) back to the main isolate via Isolate.exit internally — zero-copy for the return value.compute()compute() is Flutter's wrapper that falls back gracefully on Flutter Web (runs on the main thread there, since web doesn't support isolates).
// Equivalent to Isolate.run on mobile/desktop, runs on main thread on web
Future<List<Photo>> getPhotos(String jsonString) async {
return compute(_parsePhotos, jsonString);
}
// Top-level or static function only — closures are NOT supported by compute()
List<Photo> _parsePhotos(String jsonString) {
final data = jsonDecode(jsonString) as List<Object?>;
return data.cast<Map<String, Object?>>().map(Photo.fromJson).toList();
}Constraint: The callback must be a top-level or static function. Closures are not supported.
Use Isolate.spawn() + ports when you need to send multiple requests to the same isolate over time (avoids spawn overhead per call). Implementation pattern:
Quick structure overview:
class Worker {
final SendPort _commands; // main → worker
final ReceivePort _responses; // worker → main
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
bool _closed = false;
static Future<Worker> spawn() async { /* ... */ }
Future<Object?> parseJson(String message) async { /* ... */ }
void close() { _closed = true; _responses.close(); }
}The two-way port handshake pattern:
ReceivePort, passes its sendPort to Isolate.spawn()ReceivePort, sends its sendPort backCompleter + incrementing IDsSince Flutter 3.7, you can call platform plugins (e.g., shared_preferences, native crypto APIs) from background isolates using BackgroundIsolateBinaryMessenger.
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
// Must capture token on the main isolate before spawning
final RootIsolateToken token = RootIsolateToken.instance!;
Isolate.spawn(_isolateMain, token);
}
Future<void> _isolateMain(RootIsolateToken token) async {
// Register BEFORE using any platform plugins
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
final prefs = await SharedPreferences.getInstance();
print(prefs.getBool('isDebug'));
}Every ReceivePort creates a native resource that MUST be explicitly closed. Forgetting this causes memory leaks that accumulate over time.
❌ Memory Leak (BAD):
// DANGEROUS: ReceivePort never closed!
Future<String> badExample() async {
final receivePort = ReceivePort();
await Isolate.spawn(_worker, receivePort.sendPort);
final result = await receivePort.first; // Gets result but doesn't close port
return result as String; // LEAK: receivePort still alive!
}✅ Always Close ReceivePort (GOOD):
Future<String> goodExample() async {
final receivePort = ReceivePort();
try {
await Isolate.spawn(_worker, receivePort.sendPort);
final result = await receivePort.first; // Auto-closes after first message
return result as String;
} finally {
receivePort.close(); // Explicit close for safety
}
}receivePort.first for Auto-CleanupThe .first getter returns the first message then automatically closes the port. This is the safest pattern for single-response isolates:
Future<Map<String, dynamic>> parseInBackground(String json) async {
final port = ReceivePort();
await Isolate.spawn(_parse, (port.sendPort, json));
return await port.first as Map<String, dynamic>; // Auto-closes
}For isolates that receive multiple messages, track the port lifecycle explicitly:
class WorkerPool {
final ReceivePort _responses = ReceivePort();
Isolate? _isolate;
Future<void> start() async {
_isolate = await Isolate.spawn(_worker, _responses.sendPort);
}
Future<void> dispose() async {
_isolate?.kill(priority: Isolate.immediate);
_responses.close(); // MUST close to prevent leak
}
}Each isolate has ~2MB overhead. Spawning hundreds causes resource exhaustion.
❌ BAD: Creates 100 isolates (200MB+):
// TERRIBLE: Spawns new isolate for each item!
for (var i = 0; i < 100; i++) {
await compute(processImage, images[i]); // 100 spawns!
}✅ GOOD: Reuse a Worker Pool:
// Spawn once, send 100 tasks
final worker = await Worker.spawn();
for (var image in images) {
await worker.processImage(image); // Reuses same isolate
}
worker.close();Only primitive types and certain objects can cross isolate boundaries.
✅ Allowed Types:
int, double, String, bool, nullList, Map, Set (of serializable types)SendPort, Capability, TransferableTypedData❌ Forbidden (Will Throw):
Sendable in Dart 3.3+)❌ BAD:
class User {
final String name;
User(this.name);
}
// CRASH: User is not serializable
await compute(_process, User('Alice'));✅ GOOD:
// Send primitive data
await compute(_process, {'name': 'Alice'});
// In worker, reconstruct object
User _process(Map<String, dynamic> data) {
return User(data['name'] as String);
}compute() for Long-Running Taskscompute() spawns and tears down the isolate on every call. For repeated work, this overhead kills performance.
❌ BAD: Repeated `compute()` calls:
// Spawns 50 times!
for (var i = 0; i < 50; i++) {
await compute(heavyTask, data[i]);
}✅ GOOD: Use `Isolate.spawn()` for repeated work:
final worker = await Worker.spawn();
for (var i = 0; i < 50; i++) {
await worker.runTask(data[i]); // Same isolate, 50 tasks
}
worker.close();When you need to process multiple items over time, reuse a single isolate instead of spawning repeatedly.
class Worker {
final SendPort _commands;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
Worker._(this._commands, this._responses) {
_responses.listen((message) {
final (id, result) = message as (int, Object?);
_activeRequests.remove(id)?.complete(result);
});
}
static Future<Worker> spawn() async {
final receivePort = ReceivePort();
await Isolate.spawn(_workerMain, receivePort.sendPort);
final sendPort = await receivePort.first as SendPort;
return Worker._(sendPort, ReceivePort()..listen(...));
}
Future<T> execute<T>(Object? message) {
final id = _idCounter++;
final completer = Completer<T>();
_activeRequests[id] = completer;
_commands.send((id, message));
return completer.future;
}
void dispose() {
_responses.close(); // CRITICAL: Prevent leak
}
}| Scenario | Pattern |
|---|---|
| Process items sequentially | Single long-lived worker |
| Process items in parallel (up to CPU cores) | Worker pool (2-4 isolates) |
| UI blocking < 16ms | No isolate needed (runs fast enough) |
| Occasional heavy task | compute() or Isolate.run() |
SendPort.send() — mutating them in the worker does not affect the main isolate.String, unmodifiable Uint8List) send a reference for performance.Isolate.exit() transfers ownership (zero-copy) — used internally by Isolate.run() and compute().| Limitation | Detail |
|---|---|
| Web | Isolates not supported on Flutter Web; use compute() as a cross-platform shim |
| rootBundle / dart:ui | Not accessible inside background isolates; load assets on main isolate first |
| UI operations | No widget or rendering calls allowed in background isolates |
| Plugin push messages | Cannot receive unsolicited messages from host platform (e.g., no Firestore listener in background isolate); you can query but not subscribe |
| Shared mutable state | Global variables are copied at spawn time — changes in the worker never reflect back |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.