关于在Flutter中使用photo_manager在Android 10+设备上无需系统选择器实现跨相册媒体迁移的技术问询
Great question! Let's break down the solution based on Android 10+ Scoped Storage rules and the capabilities of the photo_manager package, with clear distinctions between scenarios where you can avoid special permissions and where you might need them.
Core Conclusion
Whether you need the MANAGE_EXTERNAL_STORAGE permission depends entirely on who created the media:
- For media files your app created: You can implement a fully Scoped Storage-friendly move without special permissions or system pickers.
- For media files created by other apps/the system: You will need the
MANAGE_EXTERNAL_STORAGEpermission to move files directly in-app (since Scoped Storage blocks modifying or writing to media owned by other entities without system intervention). Google Play has strict policies for this permission, so only use it if your app's core function requires full file management.
Scenario 1: Moving Media Your App Created (Scoped Storage-Friendly)
Scoped Storage allows your app to modify metadata and relocate media it owns without extra permissions. The key is to update the media's entry in the MediaStore to point to the target album, which will reflect the change in gallery apps.
Implementation Steps
- Identify the Target Album's Path:
Get therelativePathof your targetAssetPathEntity(e.g.,DCIM/MyAppTargetAlbum/). - Update MediaStore Metadata via Native Channel:
photo_managerdoesn't natively expose a way to modifyMediaStoremetadata, so you'll need a Flutter platform channel to call Android native code. This updates the media'sRELATIVE_PATH(which controls which album it appears in) without moving the physical file immediately (though some Android versions will automatically relocate the file for you). - Refresh
photo_managerCache:
Forcephoto_managerto refresh its local cache to reflect the new album assignment.
Code Example
Flutter Side (Platform Channel)
import 'package:flutter/services.dart'; import 'package:photo_manager/photo_manager.dart'; const _mediaMoverChannel = MethodChannel('com.yourdomain.app/media_mover'); Future<bool> moveOwnMedia(AssetEntity media, String targetRelativePath) async { try { // Pass the media's MediaStore URI and target path to native code final success = await _mediaMoverChannel.invokeMethod<bool>( 'updateMediaStorePath', { 'mediaUri': media.uri.toString(), 'targetRelativePath': targetRelativePath, }, ); if (success == true) { // Refresh photo_manager's cache to show changes in-app await PhotoManager.clearFileCache(); await PhotoManager.refreshGallery(); return true; } return false; } on PlatformException catch (e) { print('Error moving media: ${e.message}'); return false; } }
Android Native Side (MainActivity.kt)
import android.net.Uri import android.provider.MediaStore import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { private val CHANNEL = "com.yourdomain.app/media_mover" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "updateMediaStorePath" -> { val mediaUri = Uri.parse(call.argument<String>("mediaUri")) val targetPath = call.argument<String>("targetRelativePath") val success = updateMediaStoreEntry(mediaUri, targetPath) result.success(success) } else -> result.notImplemented() } } } private fun updateMediaStoreEntry(mediaUri: Uri, targetRelativePath: String): Boolean { return try { val contentValues = android.content.ContentValues().apply { // Use MediaStore.Video.Media.RELATIVE_PATH for videos put(MediaStore.Images.Media.RELATIVE_PATH, targetRelativePath) } // Update the MediaStore entry for the media contentResolver.update(mediaUri, contentValues, null, null) > 0 } catch (e: SecurityException) { // This should only happen if the media isn't owned by your app false } } }
Scenario 2: Moving Media Created by Other Apps/The System
For media not owned by your app, Scoped Storage blocks modifying its MediaStore entry or writing to arbitrary album directories without system pickers. The only in-app way to do this is with the MANAGE_EXTERNAL_STORAGE permission.
Implementation Steps
- Request
MANAGE_EXTERNAL_STORAGEPermission:
Add the permission to yourAndroidManifest.xmland request it at runtime using thepermission_handlerpackage. - Physically Move the File:
Use Dart'sFileAPIs to move the media file to the target album's directory. - Update
MediaStore:
Delete the originalMediaStoreentry (viaphoto_manager) and rescan the new file to add it to the target album's entry.
Code Example
import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; Future<bool> moveExternalMedia(AssetEntity media, AssetPathEntity targetAlbum) async { // Request MANAGE_EXTERNAL_STORAGE permission final permissionStatus = await Permission.manageExternalStorage.request(); if (!permissionStatus.isGranted) return false; final originalFile = await media.file; if (originalFile == null) return false; // Build target file path final fileName = originalFile.path.split('/').last; final targetDirPath = targetAlbum.isAll ? '/storage/emulated/0/DCIM' : targetAlbum.path; final targetFile = File('$targetDirPath/$fileName'); try { // Physically move the file await originalFile.rename(targetFile.path); // Remove original MediaStore entry await PhotoManager.editor.deleteWithIds([media.id]); // Scan the new file to add it to MediaStore await PhotoManager.editor.saveImage( await targetFile.readAsBytes(), title: fileName, relativePath: targetAlbum.relativePath, ); // Refresh gallery and in-app cache await PhotoManager.refreshGallery([targetFile.path]); await PhotoManager.clearFileCache(); return true; } catch (e) { print('Move failed: $e'); return false; } }
Critical Notes
- Google Play Policies for
MANAGE_EXTERNAL_STORAGE:
Google restricts this permission to apps that act as full file managers, backup/restore tools, or have other core use cases that require universal file access. Your app will be rejected if you request this permission without a valid reason. - Avoiding System Pickers:
All the methods above keep all selection and operations within your app—no system pickers are triggered, as required. - Testing Edge Cases:
- Test on multiple Android versions (10, 11, 12+) since Scoped Storage behavior varies slightly between versions.
- Handle cases where the target album doesn't exist (use
photo_managerto create it first if needed).
内容来源于stack exchange

