Flutter Lesson 63 of 83 4 min read
Image Picker and Camera in Flutter
Let users take a photo or choose one from the gallery in Flutter with image_picker, then display, resize and upload it.
On this page
Choosing a profile photo, attaching a receipt, posting a picture: most apps need images from the user. The image_picker plugin opens the system camera or gallery and returns the chosen file.
flutter pub add image_picker
Setup #
iOS: add to ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose a photo for your profile.</string>
<key>NSCameraUsageDescription</key>
<string>Take a photo for your profile.</string>
Android: no entries are needed for the gallery on current versions, since the system photo picker is used. Check the plugin’s README for the camera on older versions.
Picking an image #
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class AvatarPicker extends StatefulWidget {
const AvatarPicker({super.key});
@override
State<AvatarPicker> createState() => _AvatarPickerState();
}
class _AvatarPickerState extends State<AvatarPicker> {
final _picker = ImagePicker();
XFile? _image;
Future<void> _pick(ImageSource source) async {
try {
final picked = await _picker.pickImage(
source: source,
maxWidth: 1200, // shrink large photos
imageQuality: 85, // compress JPEGs
);
if (picked == null) return; // the user cancelled
setState(() => _image = picked);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open the camera or gallery.')),
);
}
}
Future<void> _choose() async {
final source = await showModalBottomSheet<ImageSource>(
context: context,
showDragHandle: true,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_camera),
title: const Text('Take a photo'),
onTap: () => Navigator.pop(context, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choose from gallery'),
onTap: () => Navigator.pop(context, ImageSource.gallery),
),
],
),
),
);
if (source != null) await _pick(source);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _choose,
child: CircleAvatar(
radius: 56,
backgroundImage: _image == null ? null : FileImage(File(_image!.path)),
child: _image == null ? const Icon(Icons.add_a_photo, size: 32) : null,
),
);
}
}
Three things to handle every time:
- The result is nullable. The user can back out.
- Wrap the call in
try/catch. Permission may be denied, or there may be no camera. - Set
maxWidthandimageQuality. A phone photo can be 12 megabytes; an avatar needs a fraction of that.
Displaying the result #
Image.file(File(_image!.path), fit: BoxFit.cover)
File comes from dart:io, which is not available on the web. For code that must run on the web too, read the bytes and use Image.memory:
final bytes = await picked.readAsBytes();
Image.memory(bytes)
Several images, or a video #
final images = await _picker.pickMultiImage(maxWidth: 1600, limit: 5);
final video = await _picker.pickVideo(
source: ImageSource.camera,
maxDuration: const Duration(seconds: 30),
);
Uploading to a server #
Images are sent as a multipart request.
import 'package:http/http.dart' as http;
Future<void> uploadAvatar(XFile image, String token) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('https://api.example.com/me/avatar'),
)
..headers['Authorization'] = 'Bearer $token'
..files.add(await http.MultipartFile.fromPath('avatar', image.path));
final response = await request.send();
if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception('Upload failed (${response.statusCode})');
}
}
The field name ('avatar' here) must match what the server expects. For progress reporting or cancelling, use the dio package.
With Firebase, upload to Cloud Storage and save the download URL in Firestore:
final ref = FirebaseStorage.instance.ref('avatars/$userId.jpg');
await ref.putFile(File(image.path));
final url = await ref.getDownloadURL();
Keeping the image #
A picked file sits in a temporary location that the system may clear. To keep it, copy it into the app’s own folder (see files and path_provider) and store the new path.
Android: lost data after returning from the camera #
On phones with little memory, Android may close your app while the camera is open. image_picker can recover the photo when the app restarts:
Future<void> _recoverLostPhoto() async {
final response = await _picker.retrieveLostData();
if (response.isEmpty) return;
final file = response.file;
if (file != null) setState(() => _image = file);
}
Call it from initState on the screen that picks images.
Cropping #
image_picker does not crop. The image_cropper plugin opens a native cropping screen, which is what users expect for profile photos.
When to use the camera plugin instead #
image_picker hands control to the system camera app. The camera plugin gives you a live preview inside your own screen, with control over the lens, flash and resolution. Use it for a custom camera interface, document scanning or anything that processes frames. For QR codes and barcodes, mobile_scanner is purpose-built.
Try it yourself #
Build a “new post” screen: a button to add up to four photos shown as a grid of thumbnails, each with a remove button, a caption field, and a Post button that is disabled until there is at least one photo.