Flutter如何获取Android公共Documents/Download文件夹存储权限?
解决Flutter Android保存文件到公共Download目录权限被拒问题
1. 修正权限配置与申请逻辑
- AndroidManifest.xml 权限调整:确保权限声明适配不同Android版本,且
requestLegacyExternalStorage配置正确:<!-- Android 10及以下所需的存储权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/> <!-- Android 11+ 写入公共Download目录无需WRITE_EXTERNAL_STORAGE,不要加MANAGE_EXTERNAL_STORAGE(仅特殊场景需要) --> <application ... android:requestLegacyExternalStorage="true"> - 权限申请代码:针对
permission_handler 9.2.0适配版本逻辑:import 'package:permission_handler/permission_handler.dart'; Future<bool> requestStoragePermission() async { final status = await Permission.storage.request(); if (status.isGranted) { return true; } else if (status.isPermanentlyDenied) { await openAppSettings(); return false; } return false; }
2. 用MediaStore API替代硬编码路径
Scoped Storage下硬编码公共目录路径会触发权限拦截,推荐通过MediaStore直接写入,无需依赖私有路径:
Flutter端代码
import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; Future<void> downloadToPublicDownload(String url, String fileName) async { final dio = Dio(); try { // 下载文件到内存 final response = await dio.get(url, options: Options(responseType: ResponseType.bytes)); final bytes = response.data as Uint8List; // 通过MethodChannel调用原生代码写入Download目录 const channel = MethodChannel('com.example.file_operations'); await channel.invokeMethod('saveToDownload', { 'fileName': fileName, 'fileBytes': bytes, }); } catch (e) { print('保存失败: $e'); } }
Android原生端实现
创建FileOperations.kt:
package com.example.your_app_package import android.content.ContentValues import android.os.Build import android.os.Environment import android.provider.MediaStore import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import java.io.FileOutputStream class FileOperations(private val engine: FlutterEngine) { private val CHANNEL = "com.example.file_operations" fun registerChannel() { MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "saveToDownload" -> { val fileName = call.argument<String>("fileName") val bytes = call.argument<ByteArray>("fileBytes") if (fileName != null && bytes != null) { saveToPublicDownload(fileName, bytes) result.success(true) } else { result.error("INVALID_PARAMS", "文件名或字节数据为空", null) } } else -> result.notImplemented() } } } private fun saveToPublicDownload(fileName: String, bytes: ByteArray) { val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, getMimeType(fileName)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) put(MediaStore.Downloads.IS_PENDING, 1) } else { val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) put(MediaStore.Downloads.DATA, "${downloadDir.absolutePath}/$fileName") } } val resolver = engine.context.contentResolver val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) uri?.let { resolver.openOutputStream(it)?.use { stream -> stream.write(bytes) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.Downloads.IS_PENDING, 0) resolver.update(it, contentValues, null, null) } } } private fun getMimeType(fileName: String): String { return when { fileName.endsWith(".pdf") -> "application/pdf" fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg" fileName.endsWith(".png") -> "image/png" fileName.endsWith(".txt") -> "text/plain" else -> "application/octet-stream" } } }
在MainActivity.kt中注册Channel:
package com.example.your_app_package import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) FileOperations(flutterEngine).registerChannel() } }
3. 依赖与版本验证
- 保持
compileSdkVersion=32、targetSdkVersion=32,避免自动升级到API33导致权限逻辑冲突 permission_handler 9.2.0适配当前compileSdk,无需升级;dio和path_provider版本可保持现有配置
4. 测试注意事项
- 卸载旧版本应用后重新安装,确保
requestLegacyExternalStorage生效 - Android 11+设备必须使用MediaStore方式写入公共目录,硬编码路径会被Scoped Storage拦截
内容的提问来源于stack exchange,提问作者markhorrocks
相关产品推荐
相关产品推荐

