custom_media_picker 0.1.3 copy "custom_media_picker: ^0.1.3" to clipboard
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
Custom Media Picker running on Android Custom Media Picker running on 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_picker ships the Dart UI plus the native handler sources, but it does not auto-register anything. Until you complete Native setup the custom_media_picker method channel has no receiver, every permission call resolves to denied, 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

License #

This project is licensed under the MIT License. See the LICENSE file for details.

3
likes
160
points
245
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter package for picking images and videos from the device gallery with a custom UI, native Android/iOS access, and preview support.

Repository (GitHub)
View/report issues

Topics

#media #picker #gallery #image #video

License

MIT (license)

Dependencies

flutter, video_player

More

Packages that depend on custom_media_picker