Images, Video & Camera
Irbisa · cheatsheetSeptember 13, 2026

Images, Video & Camera

Image pipeline, ImageCache, decode sizing, video_player, camera, audio, EXIF, thumbnails

Middle Developer16 itemscompressed for a skim
  1. 01

    You write Image.network(url) and a photo appears — what happens between that line and the pixels, and where do the caches sit?

    Easy

    The Image widget never touches bytes itself: it hands an ImageProvider to the resolution machinery, which turns a cache key into a decoded ui.Image and pushes it back through an ImageStream.

    // 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;
    …
  2. 02

    A grid of full-resolution photos kills the app even though imageCache.maximumSizeBytes is 100 MB — why doesn't that limit save you?

    Easy

    Because 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() {
    …
  3. 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?

    Medium

    cacheWidth/cacheHeight wrap your provider in a ResizeImage, which passes a target size to the codec, so the full-size ui.Image is 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);
    …
  4. 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?

    Medium

    It adds the layer Flutter has no answer for — a file cache: flutter_cache_manager writes 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,
    …
  5. 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?

    Medium

    AssetImage looks 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's scale so 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
    //
    …
  6. 06

    You precache the hero image, yet the placeholder still flashes for a frame when the detail page opens — what is going wrong?

    Medium

    Almost 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) =>
    …
  7. 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?

    Medium

    On 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';
    …
  8. 08

    Product wants a photo attachment flow — what does image_picker actually hand back, and which photo-library permission do you need for it?

    Medium

    It launches the operating system's own picker out of process and returns an XFile pointing 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,
    …
  9. 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?

    Medium

    Nothing 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. 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?

    Medium

    Decoding 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. 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?

    Medium

    video_player is a thin wrapper over ExoPlayer on Android and AVPlayer on iOS, and each of those symptoms is a field of VideoPlayerValue you are not reading.

    class ClipPlayer extends StatefulWidget {
      const ClipPlayer({super.key, required this.url});
      final Uri url;
      @override
      State<ClipPlayer> createState() => _ClipPlayerState();
    }
    …
  12. 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?

    Medium

    Adaptive 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. 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?

    Medium

    Background 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. 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?

    Hard

    The sensor, the preview and the capture each carry their own orientation, and startImageStream pushes frames onto the UI isolate faster than any Dart work can consume them — so let CameraPreview do 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. 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?

    Hard

    A Texture hands 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. 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?

    Hard

    Download 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 {
    …