flutter_photo_library

A Flutter plugin that exposes native photo-library APIs without UI.
Fetch albums, paginate images/videos, generate thumbnails, and resolve original files on Android and iOS — then build your own gallery UI.

Main highlight — fast media loading

Designed for smooth gallery scrolling: paginated native fetches, thumbnail LRU caching, and lazy original loading so the UI stays responsive — no lag on scroll, no device hang, and faster media loading even with large libraries.

Platforms Android (minSdk 24), iOS (13.0+)
Dart >=2.12.0 <4.0.0
Flutter >=2.0.0
Version 0.0.1
License MIT

Demo

Smooth gallery loading and browsing — fast media load with no lag or device hang:

flutter_photo_library demo — fast media loading


Features

Feature Description
Fast media loading Main feature — paginated native queries + cached thumbnails for lag-free grids; no UI freeze or device hang on large libraries
Smooth scrolling Loads pages and thumbs on demand so scrolling stays fluid
Local media fetch Images and videos from device storage
Albums / folders List non-empty albums; filter pages by albumId
Pagination Page index starts at 0; each page returns up to 50 items by default
Thumbnails Native generation + Dart LRU cache (max 400 entries)
Original access Raw image bytes or video file URI via getOriginalFile
Media URL Lazy local path/URI via getMediaUrl
Permissions granted / denied / permanentlyDenied
No bundled UI Data APIs only — you own the widgets

Table of contents


Install

dependencies:
  flutter_photo_library: ^0.0.1
flutter pub add flutter_photo_library
import 'package:flutter_photo_library/flutter_photo_library.dart';

Configure native platforms

Android

The plugin merges these permissions into your app automatically:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

Runtime requests by API level:

API Permissions requested
34+ READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED (limited access)
33 READ_MEDIA_IMAGES, READ_MEDIA_VIDEO
≤ 32 READ_EXTERNAL_STORAGE

To drop a permission you do not need (e.g. videos only / images only), remove it in your app AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

  <uses-permission
      android:name="android.permission.READ_MEDIA_VIDEO"
      tools:node="remove" />
</manifest>

Use compileSdk 34+.

iOS

Add to ios/Runner/Info.plist (required — missing this crashes on permission request):

<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to display your images and videos.</string>

On iOS 14+, both full (.authorized) and limited (.limited) access count as granted.


Quick start

import 'package:flutter_photo_library/flutter_photo_library.dart';

Future<void> loadGallery() async {
  final status = await FlutterPhotoLibrary.requestPermissions();
  if (status != PhotoLibraryPermissionStatus.granted) return;

  final repo = FlutterPhotoLibraryRepository();

  final albums = await repo.getAlbums(type: MediaFetchType.all);

  final items = await repo.fetchMediaPage(
    page: 0,
    pageSize: 50,
    fetchType: MediaFetchType.all,
    albumId: albums.isNotEmpty ? albums.first.id : null,
  );

  final thumb = await repo.getThumbnail(
    id: items.first.id,
    type: items.first.type,
    width: 200,
    height: 200,
  );
}

Usage

1. Permissions

Most APIs require access. Prefer checking first, then requesting if needed.

final bool alreadyGranted = await FlutterPhotoLibrary.checkPermissions();

if (!alreadyGranted) {
  final status = await FlutterPhotoLibrary.requestPermissions();

  switch (status) {
    case PhotoLibraryPermissionStatus.granted:
      break;
    case PhotoLibraryPermissionStatus.permanentlyDenied:
      // Open app Settings so the user can enable access.
      break;
    case PhotoLibraryPermissionStatus.denied:
      // Denied for now; you can ask again later.
      break;
  }
}

2. Albums (MediaAlbum)

final repository = FlutterPhotoLibraryRepository(); // singleton

final List<MediaAlbum> albums = await repository.getAlbums(
  type: MediaFetchType.all, // .all | .image | .gif
);
// MediaAlbum → id, name, count

3. Paginated media (MediaItem)

final List<MediaItem> items = await repository.fetchMediaPage(
  page: 0,                              // 0-indexed
  pageSize: FlutterPhotoLibraryRepository.defaultPageSize, // 50
  fetchType: MediaFetchType.all,
  albumId: selectedAlbum?.id,           // null = all media
);
Field Notes
id Native asset id (use with other APIs)
uri Android: content://… · iOS: asset local identifier
type MediaFetchType.image or .video
duration Milliseconds (0 for images)
width / height Pixel size
dateAdded DateTime
originalMediaUri Android: filesystem path from MediaStore when available. iOS: usually null — use getMediaUrl / getOriginalFile
formattedDuration m:ss helper for videos

4. Thumbnails

Returns JPEG Uint8List? for Image.memory. Checks the in-memory LRU cache first, then native.

final Uint8List? bytes = await repository.getThumbnail(
  id: item.id,
  type: item.type, // default: MediaFetchType.image
  width: 200,      // physical px (default 200)
  height: 200,     // physical px (default 200)
);

Tips:

  • Request physical pixels (logicalSize * devicePixelRatio) for sharp grids.
  • Cache key is id + width × height; different sizes are separate cache entries.

5. Media URL

Resolve a local path/URI when you need a file location (especially on iOS, or when originalMediaUri is null):

final String? url = await repository.getMediaUrl(
  id: item.id,
  type: item.type,
);
  • Android: typically a content:// URI
  • iOS: file URL; may download iCloud assets (networkAccessAllowed)

6. Original file

Prefer URIs for display/playback when possible. Use this when you need image bytes or a dedicated video URL:

final MediaFile? file = await repository.getOriginalFile(
  id: item.id,
  type: item.type,
);

if (file?.isImage == true) {
  // file!.bytes → Uint8List (can be large)
} else if (file?.isVideo == true) {
  // file!.videoUrl → local / content URI string
}

7. Clear Dart thumbnail cache

repository.clearCache();

Useful on leave/refresh of a large gallery.


API reference

FlutterPhotoLibrary (static)

Method Returns Description
checkPermissions() Future<bool> Granted (incl. iOS limited / Android partial)?
requestPermissions() Future<PhotoLibraryPermissionStatus> Shows system dialog when needed

FlutterPhotoLibraryRepository (singleton)

Method Returns Description
getAlbums({type}) Future<List<MediaAlbum>> Non-empty albums
fetchMediaPage({page, pageSize, fetchType, albumId}) Future<List<MediaItem>> Paginated assets
getThumbnail({id, type, width, height}) Future<Uint8List?> Thumbnail bytes (LRU)
getMediaUrl({id, type}) Future<String?> Local path / content URI
getOriginalFile({id, type}) Future<MediaFile?> Image bytes or video URI
clearCache() void Clear Dart LRU thumbnail cache

Constants: defaultPageSize = 50, defaultThumbnailWidth/Height = 200.

Enums

enum MediaFetchType { all, image, video }

enum PhotoLibraryPermissionStatus {
  granted,
  denied,
  permanentlyDenied,
}

Models

class MediaAlbum {
  final String id;
  final String name;
  final int count;
}

class MediaItem {
  final String id;
  final String uri;
  final MediaFetchType type;
  final int duration; // ms
  final int width;
  final int height;
  final DateTime dateAdded;
  final String? originalMediaUri;
  String get formattedDuration;
}

class MediaFile {
  final Uint8List? bytes;   // image
  final String? videoUrl;   // gif
  bool get isImage;
  bool get isVideo;
}

Example app

See example/ for a working gallery:

  • Permission request + permanently-denied messaging
  • Album dropdown + image / video / all filter
  • Infinite paginated grid (pageSize: 80 in the sample)
  • Thumbnail cells with physical-pixel sizing
  • Detail view via getOriginalFile (image bytes / video URI + video_player)
cd example
flutter pub get   # regenerates ios/Flutter/Generated.xcconfig for your machine
flutter run

Generated.xcconfig is gitignored on purpose (it embeds your local FLUTTER_ROOT). Always run flutter pub get after clone before pod install / Xcode.


License

MIT © MOON TECHNOLABS — see LICENSE.