A Flutter plugin that provides images, videos, and audio abstraction management APIs without interface integration, available on Android, iOS, macOS and OpenHarmony.
English | 中文
A Flutter plugin that provides assets abstraction management APIs without UI integration, you can get assets (image/video/audio) on Android, iOS, macOS and OpenHarmony.
name | pub | github |
---|---|---|
wechat_assets_picker | ||
wechat_camera_picker |
For versions upgrade across major versions, see the migration guide for detailed info.
Please search common issues in GitHub issues for build errors, runtime exceptions, etc.
Two ways to add the plugin to your pubspec:
flutter pub add photo_manager
.pubspec.yaml
's dependencies
section:dependencies:
photo_manager: $latest_version
import 'package:photo_manager/photo_manager.dart';
Minimum platform versions: Android 16, iOS 9.0, macOS 10.15.
We ship this plugin with Kotlin 1.7.22
.
If your projects use a lower version of Kotlin/Gradle/AGP,
please upgrade them to a newer version.
More specifically:
gradle-wrapper.properties
)
to 7.5.1
or the latest version.ext.kotlin_version
)
to 1.7.22
or the latest version.com.android.tools.build:gradle
)
to 7.2.2
or the latest version.If you're not setting your compileSdkVersion
or targetSdkVersion
to 29,
you can skip this section.
On Android 10, Scoped Storage was introduced, which causes the origin resource file not directly inaccessible through it file path.
If your compileSdkVersion
or targetSdkVersion
is 29
,
you can consider adding android:requestLegacyExternalStorage="true"
to your AndroidManifest.xml
in order to obtain resources:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.fluttercandies.photo_manager_example">
<application
android:label="photo_manager_example"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
</application>
</manifest>
Note: Apps that are using the flag will be rejected from the Google Play.
This is not a requirement, the plugin can still work with caching files.
But you'll need to control caches on your own, the best practice is to clear file caches
each time when you start your app by calling PhotoManager.clearFileCache()
.
The plugin use Glide to create thumbnail bytes for Android.
If you found some warning logs with Glide appearing,
it means the main project needs an implementation of AppGlideModule
.
See Generated API for the implementation.
Define the NSPhotoLibraryUsageDescription
key-value in the ios/Runner/Info.plist
:
<key>NSPhotoLibraryUsageDescription</key>
<string>In order to access your photo library</string>
If you want to grant only write-access to the photo library on iOS 11 and above,
define the NSPhotoLibraryAddUsageDescription
key-value in the ios/Runner/Info.plist
.
It's pretty much the same as the NSPhotoLibraryUsageDescription
.
Most of the APIs can only use with granted permission.
final PermissionState ps = await PhotoManager.requestPermissionExtend(); // the method can use optional param `permission`.
if (ps.isAuth) {
// Granted
// You can to get assets here.
} else if (ps.hasAccess) {
// Access will continue, but the amount visible depends on the user's selection.
} else {
// Limited(iOS) or Rejected, use `==` for more precise judgements.
// You can call `PhotoManager.openSetting()` to open settings for further steps.
}
But if you're pretty sure your callers will be only called after the permission is granted, you can ignore permission checks:
PhotoManager.setIgnorePermissionCheck(true);
For background processing (such as when the app is not in the foreground), ignore permissions check would be proper solution.
With iOS 14 released, Apple brought a "Limited Photos Library" permission
(PermissionState.limited
) to iOS.
The PhotoManager.requestPermissionExtend()
method will return PermissionState
.
See PHAuthorizationStatus for more detail.
To reselect accessible entities for the app,
use PhotoManager.presentLimited()
to call the modal of
accessible entities' management.
This method only available for iOS 14+ and when the permission state
is limited (PermissionState.limited
).
To suppress the automatic prompting from the system
when each time you access the media after the app has restarted,
you can set the Prevent limited photos access alert
key to YES
in your app's Info.plist
(or manually writing as below):
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<true/>
Android 14 (API 34) has also introduced the concept of limited assets similar to iOS.
However, there is a slight difference in behavior (based on the emulator):
On Android, the access permission to a certain resource cannot be revoked once it is granted,
even if it hasn't been selected when using presentLimited
in future actions.
AssetPathEntity
)Albums or folders are abstracted as the AssetPathEntity
class.
It represents a bucket in the MediaStore
on Android,
and the PHAssetCollection
object on iOS/macOS.
To get all of them:
final List<AssetPathEntity> paths = await PhotoManager.getAssetPathList();
See getAssetPathList
for more detail.
getAssetPathList
Name | Description | Default value |
---|---|---|
hasAll | Set to true when you need an album containing all assets. | true |
onlyAll | Use this property if you only need one album containing all assets. | false |
type | Type of media resource (video, image, audio) | RequestType.common |
filterOption | Used to filter resource files, see Filtering for details | FilterOptionGroup() |
pathFilterOption | Only valid for iOS and macOS, for the corresponding album type. See PMPathFilterOption for more detail. | Include all by default. |
Provide since 2.7.0, currently only valid for iOS and macOS.
final List<PMDarwinAssetCollectionType> pathTypeList = []; // use your need type
final List<PMDarwinAssetCollectionSubtype> pathSubTypeList = []; // use your need type
final darwinPathFilterOption = PMDarwinPathFilter(
type: pathTypeList,
subType: pathSubTypeList,
);
PMPathFilter pathFilter = PMPathFilter();
The PMDarwinAssetCollectionType
has a one-to-one correspondence with PHAssetCollectionType | Apple Developer Documentation.
The PMDarwinAssetCollectionSubtype
has a one-to-one correspondence with PHAssetCollectionSubType | Apple Developer Documentation.
AssetEntity
)Assets (images/videos/audios) are abstracted as the AssetEntity
class.
It represents a series of fields with MediaStore
on Android,
and the PHAsset
object on iOS/macOS.
AssetPathEntity
You can use the pagination method:
final List<AssetEntity> entities = await path.getAssetListPaged(page: 0, size: 80);
Or use the range method:
final List<AssetEntity> entities = await path.getAssetListRange(start: 0, end: 80);
PhotoManager
(Since 2.6)First, You need get count of assets:
final int count = await PhotoManager.getAssetCount();
Then, you can use the pagination method:
final List<AssetEntity> entities = await PhotoManager.getAssetListPaged(page: 0, pageCount: 80);
Or use the range method:
final List<AssetEntity> entities = await PhotoManager.getAssetListRange(start: 0, end: 80);
Note: page
and start
starts from 0.
The ID concept represents:
MediaStore
on Android.localIdentifier
field of the PHAsset
on iOS.You can store the ID if you want to implement features
that's related to persistent selections.
Use AssetEntity.fromId
to retrieve the entity
once you persist an ID.
final AssetEntity? asset = await AssetEntity.fromId(id);
Be aware that the created asset might have limited access or got deleted in anytime, so the result might be null.
You can create an entity from raw data, such as downloaded images, recorded videos, etc. The created entity will show as a corresponding resource on your device's gallery app.
final Uint8List rawData = yourRawData;
// Save an image to an entity from `Uint8List`.
final AssetEntity? entity = await PhotoManager.editor.saveImage(
rawData,
title: 'write_your_own_title.jpg', // Affects EXIF reading.
);
// Save an existed image to an entity from it's path.
final AssetEntity? imageEntityWithPath = await PhotoManager.editor.saveImageWithPath(
path, // Use the absolute path of your source file, it's more like a copy method.
title: 'same_as_above.jpg',
);
// Save a video entity from `File`.
final File videoFile = File('path/to/your/video.mp4');
final AssetEntity? videoEntity = await PhotoManager.editor.saveVideo(
videoFile, // You can check whether the file is exist for better test coverage.
title: 'write_your_own_title.mp4',
);
// [iOS only] Save a live photo from image and video `File`.
// This only works when both image and video file were part of same live photo.
final File imageFile = File('path/to/your/live_photo.heic');
final File videoFile = File('path/to/your/live_video.mp4');
final AssetEntity? entity = await PhotoManager.editor.darwin.saveLivePhoto(
imageFile: imageFile,
videoFile: videoFile,
title: 'write_your_own_title.heic',
);
Be aware that the created asset might have limited access or got deleted in anytime, so the result might be null.
Resources might be saved only on iCloud to save disk space.
When retrieving file from iCloud, the speed is depended on the network condition,
which might be very slow that makes users feel anxious.
To provide a responsive user interface, you can use PMProgressHandler
to retrieve the progress when load a file.
The preferred implementation would be the LocallyAvailableBuilder
in the wechat_asset_picker
package, which provides a progress indicator
when the file is downloading.
Starts from v3.0.0,
AssetEntityImage
andAssetEntityImageProvider
are provided in thephoto_manager_image_provider
package.
Use the AssetEntityImage
widget or the AssetEntityImageProvider
to display assets:
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
final Widget image = AssetEntityImage(
yourAssetEntity,
isOriginal: false, // Defaults to `true`.
thumbnailSize: const ThumbnailSize.square(200), // Preferred value.
thumbnailFormat: ThumbnailFormat.jpeg, // Defaults to `jpeg`.
);
final Widget imageFromProvider = Image(
image: AssetEntityImageProvider(
yourAssetEntity,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(200),
thumbnailFormat: ThumbnailFormat.jpeg,
),
);
This plugin supports obtain live photos and filtering them:
This is supported when filtering only image.
final List<AssetPathEntity> paths = await PhotoManager.getAssetPathList(
type: RequestType.image,
filterOption: FilterOptionGroup(onlyLivePhotos: true),
);
final AssetEntity entity = livePhotoEntity;
final String? mediaUrl = await entity.getMediaUrl();
final File? imageFile = await entity.file;
final File? videoFile = await entity.fileWithSubtype;
final File? originImageFile = await entity.originFile;
final File? originVideoFile = await entity.originFileWithSubtype;
Due to the privacy policy issues on Android 10, it is necessary to grant the location permission to obtain the original data with the location info and the EXIF metadata.
If you want to use the location permission,
add the ACCESS_MEDIA_LOCATION
permission to your manifest.
The originFile
and originBytes
getter
will return the original data of an entity.
However, there are some cases that the original data is invalid in Flutter.
Here are some common cases:
.file
if the file is HEIC) in order to keep
a consistent behavior between multiple platforms.
See flutter/flutter#20522 for more detail.There are several I/O methods in this library targeting AssetEntity
,
typically they are:
file
.AssetEntity.originBytes
.File retrieving and caches are limited by the sandbox mechanism on iOS.
An existing PHAsset
doesn't mean the file located on the device.
In generally, a PHAsset
will have three status:
isLocallyAvailable
equals true
, also cached: Available for obtain.isLocallyAvailable
equals true
, but not cached: When you call I/O methods,
the resource will first cache into the sandbox, then available for obtain.isLocallyAvailable
equals false
: Typically this means the asset exists,
but it's saved only on iCloud, or some videos that not exported yet.
In this case, the best practise is to use the PMProgressHandler
to provide a responsive user interface.Plugin will post entities change events from native,
but they will include different contents.
See the logs
folder for more recorded logs.
To register a callback for these events, use
PhotoManager.addChangeCallback
to add a callback,
and use PhotoManager.removeChangeCallback
to remove the callback,
just like addListener
and removeListener
methods.
After you added/removed callbacks, you can call
PhotoManager.startChangeNotify
method to enable to notify,
and PhotoManager.stopChangeNotify
method to stop notify.
import 'package:flutter/services.dart';
void changeNotify(MethodCall call) {
// Your custom callback.
}
/// Register your callback.
PhotoManager.addChangeCallback(changeNotify);
/// Enable change notify.
PhotoManager.startChangeNotify();
/// Remove your callback.
PhotoManager.removeChangeCallback(changeNotify);
/// Disable change notify.
PhotoManager.stopChangeNotify();
Filtering assets are also supported by the plugin.
Below methods have the filterOption
argument to define the conditions when obtaining assets.
AssetPathEntity.filterOption
)There are two implementations of filters:
The FilterOptionGroup
is the only implementation before v2.6.0.
final FilterOptionGroup filterOption = FilterOptionGroup(
imageOption: FilterOption(
sizeConstraint: SizeConstraint(
maxWidth: 10000,
maxHeight: 10000,
minWidth: 100,
minHeight: 100,
ignoreSize: false,
),
),
videoOption: FilterOption(
durationConstraint: DurationConstraint(
min: Duration(seconds: 1),
max: Duration(seconds: 30),
allowNullable: false,
),
),
createTimeCondition: DateTimeCondition(
min: DateTime(2020, 1, 1),
max: DateTime(2020, 12, 31),
),
orders: [
OrderOption(
type: OrderOptionType.createDate,
asc: false,
),
],
/// other options
);
NOTE: The CustomFilter
is still a newbie of the plugin that introduced in v2.6.0,
please help us to improve it by submitting issues if you've met any related problems.
CustomFilter
provides flexible filtering conditions, and it's targeting host platforms.
The usages of the filter are more look close to native integrations,
which is a SQL-like
filter.
The SQL columns name on Android and Darwin (iOS/macOS) are different,
you'll need to use CustomColumns.base
, CustomColumns.android
and CustomColumns.darwin
to get the columns name correctly.
An example for how to construct a CustomFilter
:
CustomFilter createFilter() {
return CustomFilter.sql(
where: '${CustomColumns.base.width} > 100 AND ${CustomColumns.base.height} > 200',
orderBy: [OrderByItem.desc(CustomColumns.base.createDate)],
);
}
AdvancedCustomFilter
is a subclass of CustomFilter
,
it has builder methods to help create the filter.
CustomFilter createFilter() {
final group = WhereConditionGroup()
.and(
ColumnWhereCondition(
column: CustomColumns.base.width,
value: '100',
operator: '>',
),
)
.or(
ColumnWhereCondition(
column: CustomColumns.base.height,
value: '200',
operator: '>',
),
);
final filter = AdvancedCustomFilter()
.addWhereCondition(group)
.addOrderBy(column: CustomColumns.base.createDate, isAsc: false);
return filter;
}
CustomFilter
: The base class of custom filter.SqlCustomFilter
: Creates a SQL-like filter.AdvancedCustomFilter
: Creates an advanced filter.
OrderByItem
: The class of ORDER BY item.WhereConditionItem
: The class of WHERE condition item.
WhereConditionGroup
: Create a group of where condition.TextWhereCondition
: The text will not be verified if it's valid.ColumnWhereCondition
: The input column will be verified if it's valid.DateColumnWhereCondition
: Dates have different conversion methods on iOS/macOS, it helps to smoothest the platform differences.CustomColumns
: This class contains fields for different platforms.
base
: The common fields are included here, but please note that the "id" field is invalid in iOS and may even cause errors. It is only valid on Android.android
: The columns of the Android platform.darwin
: The columns of iOS/macOS platforms.Here is the flow chart to explain how advanced filter works:
Because Android 10 restricts the ability to access the resource path directly,
image caches will be generated during I/O processes.
More specifically, when the file
, originFile
and any other I/O getters are called,
the plugin will save a file in the cache folder for further use.
Fortunately, on Android 11 and above, the resource path can be obtained directly again,
but you can still use requestLegacyExternalStorage
to access files in the storage without caching them.
See Android 10 (Q, 29) for how to add the attribute.
The attribute is not required.
iOS does not directly provide APIs to access the original files of the album.
So a cached file will be generated locally
into the container of the current application
when you called file
, originFile
and any other I/O getters.
If occupied disk spaces are sensitive in your use case, you can delete it after your usage has done (iOS only).
import 'dart:io';
Future<void> useEntity(AssetEntity entity) async {
File? file;
try {
file = await entity.file;
await handleFile(file!); // Custom method to handle the obtained file.
} finally {
if (Platform.isIOS) {
file?.deleteSync(); // Delete it once the process has done.
}
}
}
You can use the PhotoManager.clearFileCache()
method
to clear all caches that generated by the plugin.
Here are caches generation on different platforms,
types and resolutions.
Platform | Thumbnail | File / Origin File |
---|---|---|
Android | Yes | Yes (Android 10+) |
iOS | No | Yes |
If your found any conflicting issues against Glide,
then you'll need to edit the android/build.gradle
file:
rootProject.allprojects {
subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.github.bumptech.glide'
&& details.requested.name.contains('glide')) {
details.useVersion '4.15.1'
}
}
}
}
}
See ProGuard for Glide if you want to know more about using ProGuard and Glide together.
When targeting Android 14 (API level 34), the following extra configs needs to be added to the manifest:
<manifest>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> <!-- If you want to use the limited access feature. -->
</manifest>
When targeting Android 13 (API level 33), the following extra configs needs to be added to the manifest:
<manifest>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- If you want to read images-->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- If you want to read videos-->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <!-- If you want to read audio-->
</manifest>
By default, iOS will retrieve system album names only in English no matter what language has been set to devices. To change the default language, see the following steps:
Open your iOS project (Runner.xcworkspace) using Xcode.
Select the project "Runner" and in the localizations table, click on the + icon.
Select the adequate language(s) you want to retrieve localized strings.
Validate the popup screen without any modification.
Rebuild your flutter project.
Now system albums label should display accordingly.
Note: Localize is not meant to customize albums name.
Warning: Features here aren't guaranteed to be fully usable across platforms and OS versions, since they involved with data modification. They can be modified/removed in any time, without following a proper version semantic.
Some APIs will make irreversible modification/deletion to data. Please be careful and implement your own test mechanism when using them.
You can preload thumbnails for entities with specified thumbnail options
using PhotoCachingManager.requestCacheAssets
or PhotoCachingManager.requestCacheAssetsWithIds
.
PhotoCachingManager().requestCacheAssets(assets: assets, option: option);
And you can stop in anytime by calling
PhotoCachingManager().cancelCacheRequest()
.
Usually, when we're previewing assets, thumbnails will be loaded instantly. But sometimes we want to preload assets to make them display faster.
The PhotoCachingManager
uses the PHCachingImageManager on iOS,
and Glide's file cache on Android.
This method will delete the asset completely from your device. Use it with extra cautious.
// Deleted IDs will returned, if it fails, the result will be an empty list.
final List<String> result = await PhotoManager.editor.deleteWithIds(
<String>[entity.id],
);
After the deletion, you can call the refreshPathProperties
method
to refresh the corresponding AssetPathEntity
in order to get latest fields.
You can use copyAssetToPath
method to "Copy" an entity
from its current position to the targeting AssetPathEntity
:
// Make sure your path entity is accessible.
final AssetPathEntity anotherPathEntity = anotherAccessiblePath;
final AssetEntity entity = yourEntity;
final AssetEntity? newEntity = await PhotoManager.editor.copyAssetToPath(
asset: entity,
pathEntity: anotherPathEntity,
); // The result could be null when the path is not accessible.
The "Copy" means differently here on Android and iOS:
(For Android 30+, this feature is blocked by system limitations currently.)
// Make sure your path entity is accessible.
final AssetPathEntity pathEntity = accessiblePath;
final AssetEntity entity = yourEntity;
await PhotoManager.editor.android.moveAssetToAnother(
entity: entity,
target: pathEntity,
);
(For Android 30+, this feature is blocked by system limitations currently.)
This will remove all items (records) that's not existed locally.
A record in Android MediaStore
could have the corresponding file deleted.
Those abnormal behaviors usually caused by operations from
file manager, helper tools or adb tool.
This operation is resource-consuming,
Please use the await
keyword to call the cleaning process
before you call another one.
await PhotoManager.editor.android.removeAllNoExistsAsset();
Some operating systems will prompt confirmation dialogs for each entities' deletion, we have no way to avoid them. Make sure you're using the correct method, and your customers accept repeatedly confirmations.
await PhotoManager.editor.android.moveToTrash(list);
The method supports Android 11 and above versions. It will move the entities to trash. Throws exception when calling on Android 11-.
PhotoManager.editor.darwin.createFolder(
name,
parent: parent, // The value should be null, the root path or other accessible folders.
);
PhotoManager.editor.darwin.createAlbum(
name,
parent: parent, // The value should be null, the root path or other accessible folders.
);
Remove the entry of the asset from the specific album. The asset won't be deleted from the device, only removed from the album.
// Make sure your path entity is accessible.
final AssetPathEntity pathEntity = accessiblePath;
final AssetEntity entity = yourEntity;
final List<AssetEntity> entities = <AssetEntity>[yourEntity, anotherEntity];
// Remove single asset from the album.
// It'll call the list method as the implementation.
await PhotoManager.editor.darwin.removeInAlbum(
yourEntity,
accessiblePath,
);
// Remove assets from the album in batches.
await PhotoManager.editor.darwin.removeAssetsInAlbum(
entities,
accessiblePath,
);
AssetPathEntity
Smart albums can't be deleted.
PhotoManager.editor.darwin.deletePath();
It supports following features for now and only support image and video types.
Feature | OpenHarmony |
---|---|
getAssetPathList | ✅ |
getAssetCountFromPath | ✅ |
fetchPathProperties | ✅ |
getAssetCount | ✅ |
getAssetListPaged | ✅ |
getOriginBytes | ✅ |
getThumb | ✅ |
getAssetListRange | ✅ |
getAssetsByRange | ✅ |
deleteWithIds | ✅ |
getColumnNames | ✅ |
saveImage | ✅ |
saveImageWithPath | ✅ |
saveVideo | ✅ |
requestPermissionExtend | ✅ |
ignorePermissionCheck | ✅ |
log | ✅ |
notify | ✅ |