Flutter · isolate
Merging Images with Isolate
How I fixed a frozen loading spinner by moving heavy image processing off the main thread.
5 min readThe Problem
In a real project, users needed to upload both sides of their ID card as a single merged image.
I found a merge function online — but the merge itself wasn't the issue.
The submit button showed a CircularProgressIndicator… but the moment the merge ran, the spinner froze. The whole app stuttered.
Why? Because the merge function runs all of this on the main thread:
- 1Read image files from storage
- 2Decode the images
- 3Create a new canvas
- 4Merge pixels one by one
- 5Save the result
All on the main thread — blocking Flutter from redrawing the UI.
Before — No Isolate
UI freezes during merge
dart
Future<File> mergeImagesVertically(File front, File back) async {
final img.Image image1 = img.decodeImage(await front.readAsBytes())!;
final img.Image image2 = img.decodeImage(await back.readAsBytes())!;
final int finalWidth = max(image1.width, image2.width);
final int finalHeight = image1.height + image2.height;
final img.Image canvas = img.copyExpandCanvas(
image1,
newWidth: finalWidth,
newHeight: finalHeight,
position: img.ExpandCanvasPosition.topLeft,
);
final img.Image finalImage = img.compositeImage(
canvas, image2, dstX: 0, dstY: image1.height,
);
final dir = await getApplicationDocumentsDirectory();
final path = join(dir.path, 'merged_${DateTime.now().millisecondsSinceEpoch}.jpg');
final file = File(path);
await file.writeAsBytes(img.encodeJpg(finalImage));
return file;
}After — With Isolate
Smooth UI while merging
dart
class MergeImagesArgs {
final String frontPath;
final String backPath;
final String outputPath;
MergeImagesArgs(this.frontPath, this.backPath, this.outputPath);
}
Future<String> _mergeImagesInIsolate(MergeImagesArgs args) async {
final front = File(args.frontPath);
final back = File(args.backPath);
final img.Image image1 = img.decodeImage(await front.readAsBytes())!;
final img.Image image2 = img.decodeImage(await back.readAsBytes())!;
final int finalWidth = max(image1.width, image2.width);
final int finalHeight = image1.height + image2.height;
final img.Image canvas = img.copyExpandCanvas(
image1,
newWidth: finalWidth,
newHeight: finalHeight,
position: img.ExpandCanvasPosition.topLeft,
);
final img.Image finalImage = img.compositeImage(
canvas, image2, dstX: 0, dstY: image1.height,
);
final file = File(args.outputPath);
await file.writeAsBytes(img.encodeJpg(finalImage));
return file.path;
}
Future<File> mergeImagesVerticallyInBackground(
File frontImage, File backImage,
) async {
final dir = await getApplicationDocumentsDirectory();
final outputPath = join(
dir.path, 'merged_${DateTime.now().millisecondsSinceEpoch}.jpg',
);
final mergedPath = await compute(
_mergeImagesInIsolate,
MergeImagesArgs(frontImage.path, backImage.path, outputPath),
);
return File(mergedPath);
}The Key Change
dart
final mergedPath = await compute(
_mergeImagesInIsolate,
MergeImagesArgs(frontImage.path, backImage.path, outputPath),
);- `compute` spawns a new isolate (parallel thread) automatically.
- `_mergeImagesInIsolate` runs inside this isolate — off the main thread.
- It receives a `MergeImagesArgs` with front path, back path, and output path.
- Returns the merged image path. Main thread stays free to animate the UI.