Images, Video & Camera
Image pipeline, ImageCache, decode sizing, video_player, camera, audio, EXIF, thumbnails
- 01
You write Image.network(url) and a photo appears — what happens between that line and the pixels, and where do the caches sit?
EasyThe
Imagewidget never touches bytes itself: it hands anImageProviderto the resolution machinery, which turns a cache key into a decodedui.Imageand pushes it back through anImageStream.// An ImageProvider is a cache key plus a loader. Nothing more. class BlobImage extends ImageProvider<BlobImage> { const BlobImage(this.id, this.store); final String id; final BlobStore store; … - 02
A grid of full-resolution photos kills the app even though imageCache.maximumSizeBytes is 100 MB — why doesn't that limit save you?
EasyBecause the limit governs only the images the cache is holding for later: a picture with a listener on screen is kept alive by the widget, is not counted in
currentSizeBytes, and cannot be evicted.// What an entry really costs: decoded pixels, not the file size. int decodedBytes(int width, int height) => width * height * 4; // 4000 x 3000 photo -> 48_000_000 bytes, whatever the JPEG weighs. // Instrument the cache instead of guessing. void dumpImageCache() { … - 03
An avatar occupies 96 logical pixels but the CDN serves a 3000×3000 JPEG, so you add cacheWidth — what does that change, and what does it not?
MediumcacheWidth/cacheHeightwrap your provider in aResizeImage, which passes a target size to the codec, so the full-sizeui.Imageis never allocated — but every byte still crosses the network.// ❌ 36 MB in RAM for a 96 dp circle, on every avatar in the list. CircleAvatar(backgroundImage: NetworkImage(user.photoUrl)); // ❌ Right idea, wrong units: 96 raw pixels is blurry on a 3x screen. Image.network(user.photoUrl, width: 96, height: 96, cacheWidth: 96); … - 04
Every avatar in the feed is refetched after a cold start. What does cached_network_image add over Image.network, and what must you still configure yourself?
MediumIt adds the layer Flutter has no answer for — a file cache:
flutter_cache_managerwrites the original bytes into the OS cache directory and indexes them in a small database, so the second launch decodes from disk instead of the network.// One manager for the feed, so its eviction policy is yours, not the default 200/30d. class FeedImages { static const key = 'feedImageCache'; static final CacheManager instance = CacheManager( Config( key, … - 05
Design hands you a logo at 1x, 2x and 3x plus a flat illustration — how does Flutter pick a variant at runtime, and when is one SVG the better ship?
MediumAssetImagelooks up every variant of the declared path in the asset manifest and picks the one whose device-pixel-ratio folder is closest to the screen, then sets the decoded image'sscaleso it lays out at the same logical size everywhere.// pubspec.yaml — declare the main path only; variants are found automatically. // flutter: // assets: // - assets/logo.png # 1x, the "natural" resolution // - assets/empty_state.svg // … - 06
You precache the hero image, yet the placeholder still flashes for a frame when the detail page opens — what is going wrong?
MediumAlmost always the precache put a different key in the cache than the widget asks for, so the page starts a fresh asynchronous load and shows the placeholder exactly as designed.
// ❌ Warmed one key, displayed another: the placeholder shows anyway. await precacheImage(NetworkImage(p.heroUrl), context); Image.network(p.heroUrl, cacheWidth: 1080); // key is ResizeImageKey — miss // ✅ One provider object, used for both. ImageProvider heroProvider(Product p, double dpr) => … - 07
The feed serves 4000×3000 originals at 4 MB each and a mid-range phone chokes on it — where does the resizing belong: client, CDN, or upload pipeline?
MediumOn the server side of the wire: the phone's job is to ask for the right variant, never to download pixels it is about to throw away.
// The ladder lives in one place, client and CDN agree on it. const List<int> kWidths = [160, 320, 640, 1280]; String variantUrl(String path, int width) => 'https://img.example.com/$path?w=$width&fm=webp&q=80'; … - 08
Product wants a photo attachment flow — what does image_picker actually hand back, and which photo-library permission do you need for it?
MediumIt launches the operating system's own picker out of process and returns an
XFilepointing at a copy in your app's temporary directory — and on current Android and iOS that needs no photo-library permission at all.final _picker = ImagePicker(); Future<List<File>> pickAttachments() async { // No permission request here: the OS picker runs out of process. final List<XFile> picked = await _picker.pickMultiImage( limit: 10, … - 09
Photos look right in the app, but the same files arrive sideways on the website and the iPhone ones will not open at all. What is going on?
MediumNothing in your pipeline is rotating pixels — the JPEG still carries an EXIF orientation tag that Flutter's decoder honours and your backend ignores, and the files that will not open are HEIC.
import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:image/image.dart' as img; import 'package:image_picker/image_picker.dart'; … - 10
Shrinking a 12-megapixel photo before upload freezes the app for two seconds. Where did those frames go, and how do you get them back?
MediumDecoding and re-encoding a 12 MP image is tens of milliseconds of pure CPU per megapixel, and you ran it on the UI isolate — the one isolate that must never be busy.
import 'dart:isolate'; import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:image/image.dart' as img; import 'package:flutter_image_compress/flutter_image_compress.dart'; … - 11
A video screen spins forever on a slow connection, is letterboxed wrong, and comes back paused after the user checks a message. What is video_player doing?
Mediumvideo_playeris a thin wrapper over ExoPlayer on Android and AVPlayer on iOS, and each of those symptoms is a field ofVideoPlayerValueyou are not reading.class ClipPlayer extends StatefulWidget { const ClipPlayer({super.key, required this.url}); final Uri url; @override State<ClipPlayer> createState() => _ClipPlayerState(); } … - 12
The product needs adaptive bitrate streaming and Widevine/FairPlay playback. How much of that does video_player give you, and where do you stop?
MediumAdaptive streaming comes free because the native players do it; DRM, downloads and track selection are simply not in the plugin's API, and that is the line where you go native.
// ✅ Adaptive streaming needs nothing but a manifest URL. final controller = VideoPlayerController.networkUrl( Uri.parse('https://cdn.example.com/asset/master.m3u8'), // Android cannot sniff an extensionless URL; iOS reads the content type. formatHint: VideoFormat.hls, // Authenticates the manifest; segment requests may not carry it. … - 13
The podcast stops the moment the screen locks and the lock screen shows no controls. What has to be true on iOS, on Android, and in your Dart code?
MediumBackground audio is a capability you declare to the OS plus a media session it can attach controls to, and on the Dart side the object that owns the player has to live outside the widget tree.
// android/app/src/main/AndroidManifest.xml // <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> // <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> // <service android:name="com.ryanheise.audioservice.AudioService" // android:foregroundServiceType="mediaPlayback" android:exported="true"> // <intent-filter><action android:name="android.media.browse.MediaBrowserService"/></intent-filter> … - 14
The camera preview is stretched, saved photos come out rotated, and the preview stutters once you start running detection on frames. How do you fix all three?
HardThe sensor, the preview and the capture each carry their own orientation, and
startImageStreampushes frames onto the UI isolate faster than any Dart work can consume them — so letCameraPreviewdo the aspect maths, lock the capture orientation, and add the back-pressure yourself.class Scanner extends StatefulWidget { const Scanner({super.key}); @override State<Scanner> createState() => _ScannerState(); } … - 15
You want live video frames inside your own widget so you can draw over them. Texture or platform view — what does each one cost per frame?
HardA
Texturehands Flutter a GPU handle the plugin already owns, so the frame never enters Dart and composites like any other layer; a platform view puts the OS's real view into the tree and makes the compositor build the Flutter scene around it.// ✅ Texture: the plugin owns the surface, Dart owns only an integer. // // Android side, once per session: // val entry = textureRegistry.createSurfaceProducer() // camera.setPreviewSurface(entry.surface) // frames never leave the GPU // result.success(entry.id()) … - 16
Commuters need last night's episodes available on the subway. What do you pre-download, where does it live on disk, and how does that cache stay bounded?
HardDownload one chosen rendition as complete files you own, put them where the OS will not delete them mid-playback, and enforce a byte budget from an index that survives a crash.
class MediaStore { MediaStore(this._db, {this.budgetBytes = 2 << 30}); // 2 GB final Database _db; final int budgetBytes; Future<Directory> _root() async { …