DEV3 Technologies home

Image picker with compression

Pick several photos from gallery or camera, downscale and recompress them at pick time, and show removable thumbnails.

FlutterUploadsflutterimagepickercompression

Code

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

class PhotoPickerController extends ChangeNotifier {
  PhotoPickerController({this.max = 6, this.maxEdge = 1600, this.quality = 80, this.maxBytes = 1500000});
  final int max, maxEdge, quality, maxBytes;
  final _picker = ImagePicker();
  final List<File> files = [];
  String? message;

  Future<void> fromGallery() async {
    final left = max - files.length;
    if (left <= 0) return _warn('You can add up to $max photos.');
    // limit/maxWidth/imageQuality make the plugin resize and re-encode natively.
    final picked = await _picker.pickMultiImage(
      maxWidth: maxEdge.toDouble(),
      maxHeight: maxEdge.toDouble(),
      imageQuality: quality,
      limit: left,
    );
    await _accept(picked);
  }

  Future<void> fromCamera() async {
    if (files.length >= max) return _warn('You can add up to $max photos.');
    final shot = await _picker.pickImage(
      source: ImageSource.camera,
      maxWidth: maxEdge.toDouble(),
      maxHeight: maxEdge.toDouble(),
      imageQuality: quality,
    );
    await _accept(shot == null ? [] : [shot]);
  }

  Future<void> _accept(List<XFile> picked) async {
    message = null;
    for (final x in picked) {
      if (files.length >= max) break;
      final f = File(x.path);
      if (await f.length() > maxBytes) {
        message = 'Some photos were still too large and were skipped.';
        continue;
      }
      files.add(f);
    }
    notifyListeners();
  }

  void remove(File f) {
    files.remove(f);
    notifyListeners();
  }

  void _warn(String text) {
    message = text;
    notifyListeners();
  }
}

class PhotoStrip extends StatelessWidget {
  const PhotoStrip({super.key, required this.controller});
  final PhotoPickerController controller;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Row(children: [
          OutlinedButton.icon(onPressed: controller.fromGallery, icon: const Icon(Icons.photo_library), label: const Text('Gallery')),
          const SizedBox(width: 8),
          OutlinedButton.icon(onPressed: controller.fromCamera, icon: const Icon(Icons.photo_camera), label: const Text('Camera')),
        ]),
        if (controller.message != null) Text(controller.message!),
        SizedBox(
          height: 88,
          child: ListView(scrollDirection: Axis.horizontal, children: [
            for (final f in controller.files)
              Stack(children: [
                Padding(
                  padding: const EdgeInsets.all(4),
                  child: ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.file(f, width: 80, height: 80, fit: BoxFit.cover, cacheWidth: 160)),
                ),
                PositionedDirectional(end: 0, top: 0, child: InkWell(onTap: () => controller.remove(f), child: const Icon(Icons.cancel, size: 20))),
              ]),
          ]),
        ),
      ]),
    );
  }
}

Needs: image_picker@^1.1.0

How it works

What it solves

Phone cameras produce photos of several megabytes. Uploading them raw is slow on mobile data and wastes storage, so listing apps should shrink images before they ever leave the device.

How it works

The controller asks image_picker to do the work natively: maxWidth, maxHeight and imageQuality make the plugin downscale and re-encode the JPEG at pick time, without decoding bitmaps in Dart. The gallery call also receives limit, so users cannot choose more photos than the slots left.

After picking, a byte-size guard skips anything still larger than maxBytes and sets a message. The strip widget shows thumbnails decoded at twice the display size via cacheWidth to keep memory low, with a cancel icon placed using PositionedDirectional.

Adapting it

Tune maxEdge and quality to your needs; 1600 pixels at 80 percent is a good listing default. To upload, read files and send them as multipart parts. If you need guaranteed output size, add a re-compress loop with the flutter_image_compress package.

Pitfalls

  • limit is honoured by newer platform versions only; the max check in _accept still protects older ones.
  • PNG input ignores imageQuality.
  • Add camera and photo library usage descriptions to the iOS plist.

How to use it

Add image_picker to pubspec.yaml and the iOS camera and photo library usage strings.

final photos = PhotoPickerController(max: 6);
PhotoStrip(controller: photos)

Upload photos.files when the form is submitted.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets