custom_media_picker 0.1.3
custom_media_picker: ^0.1.3 copied to clipboard
A Flutter package for picking images and videos from the device gallery with a custom UI, native Android/iOS access, and preview support.
Custom Media Picker #
A Flutter package that lets you pick images and videos from the device gallery using a custom UI instead of relying on a 3rd-party picker package. It uses native Android and iOS access through platform channels and supports previews, selection limits, and theming.
Features #
- Image and video selection
- Grid or list layouts
- Bottom sheet or full-screen picker
- Filtering by media type
- Selection limits and callback hooks
- Customizable color tokens
- Custom sort on the Recent tab (old → new, new → old, or a custom date range)
- Full-image and video preview support
Demo #
| Android | iOS |
|---|---|
![]() |
![]() |
Platform support #
| Platform | Supported | Backing API |
|---|---|---|
| Android | ✅ API 21+ | MediaStore |
| iOS | ✅ 12+ | PhotoKit (PHAsset) |
| Web / desktop | ❌ | no native implementation — CustomMediaPicker.pick returns null |
Installation #
flutter pub add custom_media_picker
or add it directly to your pubspec.yaml:
dependencies:
custom_media_picker: ^0.1.3
⚠️ This package is not a plugin — the native setup below is required.
custom_media_pickerships the Dart UI plus the native handler sources, but it does not auto-register anything. Until you complete Native setup thecustom_media_pickermethod channel has no receiver, every permission call resolves todenied, and the picker closes immediately without ever showing the OS permission dialog.
Native setup #
Both platforms need three things: the permission declaration, the handler source, and the channel registration.
Android #
1. Declare the permissions in android/app/src/main/AndroidManifest.xml,
directly inside <manifest> and above <application>:
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
2. Copy the handler. Take android/app/src/main/kotlin/.../MediaPickerHandler.kt
from this repository into your app's Kotlin source folder (next to
MainActivity.kt) and change the first line to your own package:
package com.yourcompany.yourapp
3. Register the channel in MainActivity.kt. Forwarding
onRequestPermissionsResult is mandatory — without it the permission dialog's
answer never reaches the handler and requestPermission() never completes:
class MainActivity : FlutterActivity() {
private var mediaPickerHandler: MediaPickerHandler? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
mediaPickerHandler = MediaPickerHandler(this)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
MediaPickerHandler.CHANNEL,
).setMethodCallHandler(mediaPickerHandler)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
if (mediaPickerHandler?.onRequestPermissionsResult(
requestCode, grantResults) == true
) return
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
iOS #
1. Declare the usage description in ios/Runner/Info.plist. iOS will not
show the permission dialog without it, and the app will not appear under
Settings → Privacy → Photos, so the user cannot grant access manually either:
<key>NSPhotoLibraryUsageDescription</key>
<string>Photos access is needed to pick images and videos from your gallery.</string>
2. Copy the handler. Paste the contents of lib/MediaPickerHandler.swift
from this repository into your ios/Runner/AppDelegate.swift, below the
AppDelegate class. Keeping it in AppDelegate.swift means the code is already
part of the Runner target — no Add Files to Runner… step in Xcode.
3. Register the channel in AppDelegate.swift. Use whichever variant
matches the template your app was generated from:
// Flutter 3.35+ (scene-based Runner, has didInitializeImplicitFlutterEngine)
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
private let mediaPickerHandler = MediaPickerHandler()
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
let channel = FlutterMethodChannel(
name: MediaPickerHandler.channelName,
binaryMessenger: engineBridge.applicationRegistrar.messenger())
channel.setMethodCallHandler { [weak self] call, result in
self?.mediaPickerHandler.handle(call, result: result)
}
}
}
// Older Runner templates (window.rootViewController is the FlutterViewController)
@main
@objc class AppDelegate: FlutterAppDelegate {
private let mediaPickerHandler = MediaPickerHandler()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: MediaPickerHandler.channelName,
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { [weak self] call, result in
self?.mediaPickerHandler.handle(call, result: result)
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Permissions reference #
| Platform | Declaration | Applies to |
|---|---|---|
| Android 13+ (API 33) | READ_MEDIA_IMAGES, READ_MEDIA_VIDEO |
full gallery read |
| Android 14+ (API 34) | READ_MEDIA_VISUAL_USER_SELECTED |
partial ("Select photos") access |
| Android ≤ 12 (API 32) | READ_EXTERNAL_STORAGE with maxSdkVersion="32" |
legacy gallery read |
| iOS | NSPhotoLibraryUsageDescription |
full + limited photo library access |
CustomMediaPicker.pick requests access on your behalf and calls
config.onPermissionDenied when the user refuses. Nothing is written to the
gallery, so NSPhotoLibraryAddUsageDescription and WRITE_EXTERNAL_STORAGE
are not needed.
Usage #
import 'package:custom_media_picker/custom_media_picker.dart';
final assets = await CustomMediaPicker.pick(
context,
config: MediaPickerConfig(
layoutStyle: LayoutStyle.sheet,
listStyle: ListStyle.grid,
gridCrossAxisCount: 3,
maxSelection: 9,
mediaFilter: MediaFilter.all,
title: 'Select media',
onPermissionDenied: () => debugPrint('Photos permission denied'),
),
);
if (assets != null) {
for (final asset in assets) {
// Already resolved when config.resolvePathsOnConfirm is true (the default);
// otherwise call `await asset.resolvePath()`.
debugPrint(asset.resolvedPath);
}
}
pick returns null when the user dismisses the picker or when permission
was denied.
Handling permission yourself #
MediaPickerChannel exposes the permission calls if you want to show your own
rationale before the system dialog, or offer a way back after a refusal:
final status = await MediaPickerChannel.checkPermission();
// 'granted' | 'limited' | 'notDetermined' | 'denied'
if (status == 'notDetermined') {
// The system dialog can still be shown.
await MediaPickerChannel.requestPermission();
} else if (status == 'denied') {
// iOS never prompts twice, and Android stops after two refusals —
// Settings is the only way back.
await MediaPickerChannel.openAppSettings();
}
limited (iOS 14+ "Selected Photos", Android 14+ partial access) is a working
state: the picker shows the subset the user granted.
Sorting #
The Recent tab can show a sort row under the tab bar with three modes:
- New to old — default once sorting is on.
- Old to new.
- Custom range — opens the picker's own date range sheet (not the stock Flutter dialog); the feed is then restricted to that window (newest first within it). Swipe or use the chevrons to page between months.
enableSort defaults to false — the picker ships with no sort row until
you turn it on:
MediaPickerConfig(
enableSort: true, // off by default; set true to show the sort row
initialSortOrder: MediaSortOrder.newestFirst,
dateRangeStartLabel: 'Start Date', // header placeholder before a start day is picked
dateRangeEndLabel: 'End Date',
dateRangeSaveLabel: 'Save',
colors: MediaPickerColors(
sortAccent: Color(0xFF25D366), // selected option / active row tint
dateRangeSurface: Color(0xFFFFFFFF), // sheet background
dateRangeHeaderText: Color(0xFFFFFFFF), // text on top of dateRangeSelectedFill
dateRangeRangeText: Color(0xFF1C1C1E),
// dateRangeSelectedFill / dateRangeRangeFill are left unset here, so
// they default to confirmButton (and confirmButton at 50% opacity for
// the in-range days) — pass your own Color to override either one.
),
)
Sorting only affects the Recent feed — album feeds are always newest-first.
Native getAssets calls now carry ascending/startDate/endDate
arguments; both native handlers default them to today's exact behavior
(newest-first, no date filter), so existing native setups keep working
without changes until you copy the updated handler files.
Example app #
The repository includes a demo app in example/ showing many configuration
options, theme presets, selection workflows, and a complete permission request
flow with a Settings fallback. It has the native setup above already applied, so
it is the fastest place to see a working integration.
cd example
flutter pub get
flutter run
Troubleshooting #
| Symptom | Cause |
|---|---|
| Picker closes instantly, no permission dialog | Native setup skipped — the method channel has no receiver, so every call falls back to denied. |
| iOS: app missing from Settings → Privacy → Photos | NSPhotoLibraryUsageDescription missing from Info.plist. |
Android: request returns denied without a dialog |
Permissions missing from AndroidManifest.xml, or the user refused twice — send them to Settings via openAppSettings(). |
Android: requestPermission() never returns |
onRequestPermissionsResult not forwarded from MainActivity. |
| Gallery is empty after granting | Status is limited; the user picked a subset. Call openAppSettings() to let them widen it. |
Notes #
- Android 14+ partial access is supported through the native media picker flow.
- iOS can resolve assets from iCloud when needed — the first
resolvePath()on an iCloud asset may take a moment while it downloads. - The package is designed to work as a reusable library while keeping the example app and native integrations in the same repository.
👨💻 Maintainer #
Adel Mostafa
Senior Flutter Developer
- 🌐 Portfolio: https://adel-portfolio-three.vercel.app
- 🐙 GitHub: https://github.com/Adelmostafa31
- 💼 LinkedIn: https://linkedin.com/in/adel-mostafa
License #
This project is licensed under the MIT License. See the LICENSE file for details.


