You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关于在Flutter中使用photo_manager在Android 10+设备上无需系统选择器实现跨相册媒体迁移的技术问询

在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:

  1. For media files your app created: You can implement a fully Scoped Storage-friendly move without special permissions or system pickers.
  2. For media files created by other apps/the system: You will need the MANAGE_EXTERNAL_STORAGE permission 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

  1. Identify the Target Album's Path:
    Get the relativePath of your target AssetPathEntity (e.g., DCIM/MyAppTargetAlbum/).
  2. Update MediaStore Metadata via Native Channel:
    photo_manager doesn't natively expose a way to modify MediaStore metadata, so you'll need a Flutter platform channel to call Android native code. This updates the media's RELATIVE_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).
  3. Refresh photo_manager Cache:
    Force photo_manager to 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

  1. Request MANAGE_EXTERNAL_STORAGE Permission:
    Add the permission to your AndroidManifest.xml and request it at runtime using the permission_handler package.
  2. Physically Move the File:
    Use Dart's File APIs to move the media file to the target album's directory.
  3. Update MediaStore:
    Delete the original MediaStore entry (via photo_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

  1. 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.
  2. Avoiding System Pickers:
    All the methods above keep all selection and operations within your app—no system pickers are triggered, as required.
  3. 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_manager to create it first if needed).

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:32:58