A Flutter plugin that splits tall images into memory-efficient chunks using platform-native bitmap decoders.
Flutter's GPU rendering engine (Skia/Impeller) has a maximum texture height limit. The exact value depends on the device, typically between 4096 and 16384 pixels. Images taller than this limit are forcefully downscaled, causing visible distortion — especially noticeable with promotional banners, infographics, and long screenshots.
No Dart-side workaround exists (not BoxFit, cacheHeight, or any image package). The limitation is at the GPU texture level.
A 1000×14284 landscape rendered with raw Image.asset (downsampled by the GPU texture limit) versus SplitImageView (chunked at full resolution). Run example/ to reproduce.
Before — Image.asset
After — SplitImageView
This plugin decodes and splits images natively (bypassing Flutter's texture limit), saves each chunk as a JPEG file, and exposes a companion widget that renders the chunks as if they were a single image.
| Platform | Decoder | Memory profile |
|---|---|---|
| Android | BitmapRegionDecoder |
Only the current chunk in memory |
| iOS | CGImageSource + region crop |
Memory-mapped source, per-chunk allocation |
dependencies:
flutter_image_splitter: ^0.2.0import 'package:flutter_image_splitter/flutter_image_splitter.dart';
final splitter = ImageSplitter();
// Split an image. maxChunkHeight defaults to the device's max GPU texture size.
final outcome = await splitter.split('https://example.com/tall-banner.jpg');
// Render with the companion widget (recommended).
SplitImageView(outcome: outcome)When you no longer need the splitter, release per-instance state:
@override
void dispose() {
splitter.dispose();
super.dispose();
}split() accepts:
https://.../http://...— remote image (downloaded and cached)file:///...— local file URI- Absolute filesystem path (
/path/to/image.jpg)
SplitImageView is a companion widget that renders a SplitOutcome with the per-chunk display heights computed up-front. This eliminates the layout-shift jank you'd see if you wired the chunks into a ListView directly.
// Standalone, scrollable page body:
SplitImageView.scrollable(outcome: outcome)
// Nested inside an existing scroller (e.g., a SliverList sibling):
SplitImageView(outcome: outcome)You can also render manually if you need full control:
Column(
children: [
for (int i = 0; i < outcome.paths.length; i++)
SizedBox(
width: width,
height: outcome.chunkHeights[i] * (width / outcome.imageWidth),
child: Image.file(File(outcome.paths[i]), fit: BoxFit.fill),
),
],
)The plugin keeps a per-source cache in the app's temporary directory:
- Cache key: SHA-256 hash of the source URL plus
maxChunkHeight(different chunk sizes don't collide). - Atomic commits: writes happen in a sibling temp directory and are renamed into place. Crashes mid-split never leave partial chunk sets.
- ETag / Last-Modified: when the server provides them, repeated requests send
If-None-Match/If-Modified-Since. A304response reuses the cache without re-decoding. - Manual invalidation: call
splitter.clearCache()to wipe everything.
final deletedCount = await splitter.clearCache();- Up to 2 concurrent splits across the plugin (configurable in a future release). This caps memory peaks while still allowing independent images to download in parallel.
- In-flight deduplication: if two callers ask for the same source at the same time, only one operation runs and the result is shared.
By default the plugin queries the device's actual GPU texture limit. You can override this:
final outcome = await splitter.split(
imageUrl,
maxChunkHeight: 4096,
);Smaller chunks reduce per-frame memory at the cost of more files.
try {
final outcome = await splitter.split(imageUrl);
} on PlatformException catch (e) {
print('Error: ${e.code} - ${e.message}');
}| Error code | Cause | Recovery |
|---|---|---|
INVALID_ARGS |
Empty source, non-positive height, malformed URL | Check input values |
WIDTH_TOO_LARGE |
Image width exceeds maxChunkHeight (horizontal split unsupported) |
Use a larger maxChunkHeight or wait for v0.3 |
DOWNLOAD_ERROR |
Network error, 404, timeout | Retry or check URL |
FILE_NOT_FOUND |
Local source path does not exist | Check the path |
DECODE_ERROR |
Unsupported format, corrupt image | Verify format |
SPLIT_ERROR |
Disk full, IO failure, internal error | Free up space, retry |
Any format supported by the platform's native image decoder:
- JPEG, PNG, WebP, GIF (first frame), BMP, HEIF/HEIC (iOS)
EXIF orientation is normalised across both the no-split and split paths, so iPhone photos taken in portrait render upright regardless of which path is taken.
- Vertical split only. Images wider than
maxChunkHeightthrowWIDTH_TOO_LARGE. Horizontal split is planned for v0.3+. - JPEG output. Chunks are always saved as JPEG (92% quality). Transparency is not preserved.
- No streaming. The full image is downloaded before splitting begins.
- Android: minSdk 24+
- iOS: 13.0+
- Flutter: 3.3.0+
See LICENSE.

