Flutter Image Picker: Pick from Camera and Gallery (2026)
Last Updated: | Author: Deep Singh
Picking an image from the device is one of the most common things a mobile app has to do — profile pictures, chat attachments, product photos, document uploads. Flutter has a dedicated plugin for exactly this: image_picker.
In this tutorial, you'll build a screen that lets the user pick an image either from the camera or from the gallery, preview it, and convert it to Base64 (a common first step before sending it to a server). We'll cover plugin setup, the current API, platform permissions, and the common pitfalls.
Heads-up: The original 2021 tutorial used PickedFile and image_picker: ^0.6.7+17. Both are outdated. In the current version of the plugin, PickedFile has been replaced by XFile, and the API for reading bytes is different. This tutorial uses the current API.
Adding the Plugin
Add the plugin with:
flutter pub add image_picker
Or add it manually to your pubspec.yaml under dependencies:
dependencies:
image_picker: ^1.1.2
Then run flutter pub get.
Version note: The original tutorial used image_picker: ^0.6.7+17, which was released in early 2021 and uses the deprecated PickedFile API. The current version (1.1.x) uses XFile — a cross-platform file abstraction that works on web too.
Platform Permissions
Both camera and gallery access require explicit permissions on modern mobile OSes. The plugin doesn't handle this for you — you need to declare them in your platform files.
Android
In android/app/src/main/AndroidManifest.xml, add these above the <application> tag:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
Note: For Android 13+ (API 33+), READ_MEDIA_IMAGES replaces the old READ_EXTERNAL_STORAGE. If you're targeting older Android versions as well, you'll need both, but the plugin's own manifest usually merges the correct one. The image_picker plugin handles the manifest merge for gallery permissions automatically in most cases — you may only need to add CAMERA explicitly.
iOS
In ios/Runner/Info.plist, add these keys:
<key>NSCameraUsageDescription</key>
<string>This app needs access to the camera to take photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to the photo library to select images.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to the microphone when recording video.</string>
Without these keys, iOS will crash the app the moment you try to open the camera or gallery.
The Full Code
Here's a complete, working example using the current API. It has two icon buttons for camera and gallery, a preview area, and an upload button that converts the image to Base64.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyImagePicker(),
));
}
class MyImagePicker extends StatefulWidget {
const MyImagePicker({super.key});
@override
State<MyImagePicker> createState() => _MyImagePickerState();
}
class _MyImagePickerState extends State<MyImagePicker> {
final ImagePicker _picker = ImagePicker();
XFile? _image;
bool _busy = false;
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? picked = await _picker.pickImage(
source: source,
imageQuality: 50,
);
if (picked == null) return; // User cancelled
setState(() {
_image = picked;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to pick image: $e')),
);
}
}
}
Future<void> _upload() async {
if (_image == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please pick an image first')),
);
return;
}
setState(() => _busy = true);
try {
final bytes = await _image!.readAsBytes();
final img64 = base64Encode(bytes);
debugPrint('Base64 length: ${img64.length}');
// TODO: send img64 to your server here
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload failed: $e')),
);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Image Picker')),
body: SingleChildScrollView(
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 20),
width: 300,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: _image != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(_image!.path),
fit: BoxFit.cover,
),
)
: const Icon(Icons.image, size: 120, color: Colors.grey),
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.camera_alt, size: 50),
tooltip: 'Take a photo',
onPressed: () => _pickImage(ImageSource.camera),
),
IconButton(
icon: const Icon(Icons.photo_library, size: 50),
tooltip: 'Pick from gallery',
onPressed: () => _pickImage(ImageSource.gallery),
),
],
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _busy ? null : _upload,
child: _busy
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Upload Me'),
),
),
),
const SizedBox(height: 30),
],
),
),
);
}
}
How the Code Works
Let's walk through the important parts.
1. The ImagePicker Instance
final ImagePicker _picker = ImagePicker();
Create one ImagePicker instance as a field of the state class. You don't need a new instance per call — the plugin is stateless and reusing the same one is fine.
2. Picking from Camera or Gallery
final XFile? picked = await _picker.pickImage(
source: source,
imageQuality: 50,
);
The source parameter takes ImageSource.camera or ImageSource.gallery. The imageQuality parameter is a value from 0 to 100 — it compresses the image before returning it. Setting it to 50 reduces the file size significantly, which matters when you'll be uploading it.
The return type is XFile? — nullable, because the user might cancel the picker. Always check for null before using the result.
3. Previewing the Image
Image.file(File(_image!.path), fit: BoxFit.cover)
XFile.path gives the file path on disk. On mobile, you can wrap it in a File and pass it to Image.file. The BoxFit.cover makes the image fill the 300×300 container while preserving its aspect ratio — it crops rather than distorts.
Note for web: dart:io's File doesn't work on web. For web support, use Image.network(_image!.path) instead, which works with the blob URL that XFile exposes.
4. Reading Bytes
final bytes = await _image!.readAsBytes();
In the current plugin, XFile has a readAsBytes() method that returns a Future<Uint8List>. The old PickedFile API required manually reading a File. This is much simpler and works on web too.
5. Base64 Encoding
final img64 = base64Encode(bytes);
base64Encode converts the raw bytes into a Base64 string. This is a common first step before sending an image to a REST API — you embed the string in JSON. Note that Base64 inflates the size by about 33%, so for large images, you'd typically use multipart form upload instead.
Warning: Base64 works for small images, but if you're uploading photos, use http.MultipartRequest or dio's form data. Sending a 5MB photo as Base64 JSON will time out or hit server limits.
6. The Busy Flag
The _busy boolean prevents the user from tapping the upload button multiple times while the operation is in flight, and it shows a spinner in place of the button text. This is a small UX detail that matters — without it, users tap again, and you end up with duplicate uploads.
Common Pitfalls
- Using the old
PickedFileAPI. The original tutorial's code will not compile againstimage_picker: ^1.x. UseXFileandreadAsBytes()instead. - Not checking for null. If the user cancels the picker,
pickImage()returnsnull. Calling_image.pathwithout a null check crashes. - Forgetting platform permissions. iOS will crash the app if the Info.plist keys are missing. Android 13+ needs
READ_MEDIA_IMAGES. Always declare permissions. - Skipping
imageQuality. Full-resolution photos from modern phones are 3–10 MB. If you're uploading, you want to compress them. SetimageQuality: 50or lower. - Reading bytes synchronously on the main thread.
File(path).readAsBytesSync()blocks the UI thread for the duration of the read. Use the asyncreadAsBytes()instead. - Sending Base64 for large images. Base64 inflates size by ~33%. For anything over a few hundred KB, use multipart upload.
- Not handling
setState()afterawait. Always checkif (mounted)before callingsetState()after an async operation, or you'll get a "setState called after dispose" error. - Using
Fileon web.dart:ioisn't available on the web. UseImage.network(_image!.path)for web previews. - Assuming camera works on emulators. iOS simulators have no camera. Android emulators do, but they use a fake camera feed. Test on a physical device.
- Not requesting runtime permission on Android. On Android 6+ the OS requires runtime permission for camera access. The plugin handles this, but if you've customized permissions, you may need
permission_handlertoo.
Wrapping Up
You now know how to pick an image from the camera or gallery in Flutter using the current image_picker API. The key updates from older tutorials are XFile instead of PickedFile, readAsBytes() for async reading, and proper platform permissions.
From here, you can extend the example with:
- Multipart upload. Use
http.MultipartRequestordioto send the image to a server without Base64 inflation. - Cropping. The
image_cropperplugin lets users crop their selected image before uploading. - Multiple image selection. Use
pickMultiImage()instead ofpickImage(). - Video picking. The same plugin supports
pickVideo()for video files. - Image compression. The
flutter_image_compresspackage gives you more control than the built-inimageQualityparameter.
1 comment:
when u lauch first time in real device and open camera click pic after right click screen will blank
Post a Comment