Flutter(Android)内部存储文件跨文件夹复制时权限被拒问题排查
首先,咱们来拆解你遇到的Permission denied (errno=13)问题,主要和Android 10及以上版本的**分区存储(Scoped Storage)**机制、权限配置以及代码细节有关,下面一步步给你解决:
1. 理解核心原因:Android 10+的存储权限限制
从Android 10(API Level 29)开始,Google引入了分区存储机制,默认情况下即使你申请了WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE,也不能直接访问公共存储目录(比如/sdcard、/sdcard/Documents)下的非应用创建文件——这是系统为保护用户隐私做的强制限制。
另外你看到的/sdcard路径是正常现象:很多Android厂商把内部存储的挂载点映射为/sdcard,真正的外置SD卡会用其他路径(比如/sdcard1),不用纠结这个路径显示。
2. 第一步:适配Manifest配置
针对Android 10(API29)
在AndroidManifest.xml的application标签里添加android:requestLegacyExternalStorage="true",暂时绕过分区存储限制,让传统存储权限生效:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.xxxx"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application ... android:requestLegacyExternalStorage="true"> <!-- 添加这一行 --> ... </application> </manifest>
针对Android 11+(API30及以上)
如果应用目标SDK是30+,requestLegacyExternalStorage会失效,此时要访问公共存储的所有文件,需要申请MANAGE_EXTERNAL_STORAGE特殊权限,先在Manifest里添加:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:minSdkVersion="30" />
3. 修正权限申请代码
你的permission_handler权限申请逻辑需要适配不同Android版本:
- Android 10及以下:用
Permission.storage - Android 11+:需要用
Permission.manageExternalStorage,且该权限无法通过弹窗申请,得引导用户去系统设置开启
修改后的权限申请代码:
import 'package:permission_handler/permission_handler.dart'; void _grantPermission () async { Permission permission; if (await Permission.manageExternalStorage.isSupported) { // Android 11+ 适配特殊权限 permission = Permission.manageExternalStorage; } else { // Android 10及以下用传统存储权限 permission = Permission.storage; } var status = await permission.status; if (status.isGranted) { print("权限已授予"); } else if (status.isDenied) { var result = await permission.request(); if (!result.isGranted && permission == Permission.manageExternalStorage) { // 引导用户去设置开启特殊权限 await openAppSettings(); } } else if (status.isPermanentlyDenied) { // 永久拒绝时,直接引导去设置页 await openAppSettings(); } }
4. 修正文件复制的代码细节
- 必须用await处理异步操作:
file.copy()是异步方法,直接调用会导致权限未完全生效就执行复制,或操作未完成就中断 - 避免硬编码路径:推荐用
path_provider包获取标准路径,适配不同设备的路径差异
先在pubspec.yaml添加path_provider依赖,再修改复制代码:
import 'package:path_provider/path_provider.dart'; import 'dart:io'; void _saveFile() async { // 先检查权限状态 Permission permission = await Permission.manageExternalStorage.isSupported ? Permission.manageExternalStorage : Permission.storage; var status = await permission.status; if (!status.isGranted) { print("权限未授予,请先申请权限"); return; } // 源文件路径(可根据实际情况调整) File sourceFile = File('/sdcard/Maths paper.pdf'); // 检查源文件是否存在 if (!await sourceFile.exists()) { print("源文件不存在"); return; } // 获取Documents目录的标准路径 Directory? documentsDir = await getExternalStoragePublicDirectory(DirectoryType.documents); if (documentsDir == null) { print("无法获取Documents目录"); return; } File targetFile = File('${documentsDir.path}/Maths paper.pdf'); try { await sourceFile.copy(targetFile.path); print("文件复制成功"); } catch (e) { print("复制失败:$e"); } }
5. 长期解决方案:适配分区存储(推荐)
上面的方法是兼容传统存储的临时方案,Google推荐的长期方案是使用**存储访问框架(SAF)**或MediaStore操作公共存储文件,无需特殊权限,更符合Android隐私规范。比如用file_picker让用户选择源文件,再复制到目标目录:
import 'package:file_picker/file_picker.dart'; void _pickAndCopyFile() async { // 让用户选择源文件 FilePickerResult? result = await FilePicker.platform.pickFiles(); if (result == null) return; File sourceFile = File(result.files.single.path!); // 获取Documents目录 Directory? documentsDir = await getExternalStoragePublicDirectory(DirectoryType.documents); if (documentsDir == null) return; File targetFile = File('${documentsDir.path}/${result.files.single.name}'); try { await sourceFile.copy(targetFile.path); print("文件复制成功"); } catch (e) { print("复制失败:$e"); } }
这种方式既不需要申请特殊权限,也不受分区存储限制,是更合规的做法。
内容的提问来源于stack exchange,提问作者LovinduLI

