Flutter Android写入Downloads文件夹遇权限拒绝(errno=13)求助
解决Android 10-13下复制文件到Downloads文件夹权限拒绝问题
问题重现
调用file.copy时触发权限错误:
PathAccessException: Cannot copy file to '/storage/emulated/0/Download/test.pdf', path = '/data/user/0/myapp/cache/file-pdf.pdf' (OS Error: Permission denied, errno = 13)
当前配置:
- 依赖
permission_handler: ^11.1.0 - AndroidManifest.xml已配置
READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE权限 - minSdkVersion 21,targetSdkVersion跟随Flutter默认(推测为33+)
核心原因
从Android 10(API 29)开始,系统启用分区存储机制,直接通过硬编码路径访问公共外部存储的方式被严格限制:
- Android 10:默认启用分区存储,
WRITE_EXTERNAL_STORAGE不再授予全局写入权限,需通过兼容标记或系统API操作公共目录 - Android 11+:彻底收紧权限,
WRITE_EXTERNAL_STORAGE仅能访问应用私有目录,无法直接写入公共Downloads文件夹 - Android 13:针对公共目录的非媒体文件,必须通过
MediaStore或存储访问框架(SAF)完成写入操作
解决方案
1. 权限配置适配
AndroidManifest.xml调整
- 针对Android 10,添加兼容标记(过渡方案,仅适用于临时兼容旧逻辑):
<application ... android:requestLegacyExternalStorage="true"> ... </application>
- Android 13无需额外添加特殊权限,使用
MediaStore即可合法写入Downloads目录
权限申请逻辑优化
不要仅调用权限请求,必须判断授权结果:
final status = await Permission.storage.request(); if (status.isDenied || status.isPermanentlyDenied) { openAppSettings(); return; }
2. 推荐实现:通过MediaStore写入Downloads(适配10+)
替代直接file.copy,借助系统MediaStore完成文件写入,无需硬编码路径:
import 'dart:io'; import 'dart:typed_data'; import 'package:permission_handler/permission_handler.dart'; import 'package:media_store_plus/media_store_plus.dart'; Future<void> copyToDownloads(File sourceFile, String fileName) async { // 权限校验 final status = await Permission.storage.request(); if (!status.isGranted) { openAppSettings(); return; } // 读取源文件字节 final fileBytes = await sourceFile.readAsBytes(); // 写入Downloads目录 final mediaStore = MediaStore(); final saveUri = await mediaStore.saveFile( path: DirectoryDownloads, bytes: fileBytes, fileName: fileName, mimeType: 'application/pdf', ); saveUri != null ? print('文件已保存到Downloads: $saveUri') : print('保存失败'); } // 调用示例 // await copyToDownloads(File('/data/user/0/myapp/cache/file-pdf.pdf'), 'test.pdf');
需在pubspec.yaml添加依赖:
media_store_plus: ^1.0.0(或最新版本)
3. 合规性最高方案:使用存储访问框架(SAF)
让用户手动选择保存位置,无需依赖特殊权限,完全符合系统规范:
import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/services.dart'; Future<void> saveViaSAF(File sourceFile, String fileName) async { const channel = MethodChannel('com.yourapp.file_save'); try { final bytes = await sourceFile.readAsBytes(); final result = await channel.invokeMethod('saveFile', { 'bytes': bytes, 'fileName': fileName, 'mimeType': 'application/pdf', }); result == true ? print('保存成功') : print('保存失败'); } on PlatformException catch (e) { print('错误:${e.message}'); } }
对应的Android原生代码(MainActivity.kt):
class MainActivity : FlutterActivity() { private val CHANNEL = "com.yourapp.file_save" private var saveBytes: ByteArray? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "saveFile" -> { saveBytes = call.argument<ByteArray>("bytes") val fileName = call.argument<String>("fileName") val mimeType = call.argument<String>("mimeType") if (saveBytes == null || fileName == null || mimeType == null) { result.error("INVALID_ARGS", "参数缺失", null) return@setMethodCallHandler } val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = mimeType putExtra(Intent.EXTRA_TITLE, fileName) } startActivityForResult(intent, 1001) result.success(true) } else -> result.notImplemented() } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 1001 && resultCode == RESULT_OK) { data?.data?.let { uri -> contentResolver.openOutputStream(uri)?.use { it.write(saveBytes) } } } } }
4. 避免硬编码路径
不要直接使用/storage/emulated/0/Download/,推荐通过官方API获取合法路径:
// Android 10以下兼容(已废弃,但可用) final downloadDir = await getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // 或使用path_provider的标准方法 final downloadDir = await getDownloadsDirectory();
总结
- 快速兼容Android 10可添加
requestLegacyExternalStorage标记,但Android 11+会失效 - 长期适配优先选择MediaStore方案,无需特殊权限且符合系统规范
- 追求最高合规性则使用SAF,让用户自主控制文件存储位置
内容的提问来源于stack exchange,提问作者Go-At Solutions
相关产品推荐
相关产品推荐

