Flutter存储至Download目录遇权限拒绝错误(errno=13)求助
解决Android公共Download目录写入权限问题及Permission-Handler日志报错
1. 解决Permission-Handler的「权限未在清单中找到」报错
日志中的[]22对应Permission.manageExternalStorage,出现该提示的核心原因:
MANAGE_EXTERNAL_STORAGE是Android 11(API 30)及以上的特殊权限,需确保App的targetSdkVersion≥30,且权限声明需适配版本。
修正步骤:
- 修改
android/app/build.gradle,确保targetSdkVersion不低于30:
android { defaultConfig { targetSdkVersion 33 // 推荐使用最新稳定版本 } }
- 调整Manifest权限声明,适配不同Android版本:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Android 11+ 全文件访问权限 --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:minSdkVersion="30"/> <!-- Android 10及以下 存储读写权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/> <application android:label="@string/app_name" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"> ... </application> </manifest>
2. 修正权限申请逻辑
Permission.storage在Android 13+已不再对应公共存储权限,且MANAGE_EXTERNAL_STORAGE无法通过弹窗直接申请,必须引导用户到设置页手动开启:
import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; Future<void> checkAndRequestPermissions() async { if (!Platform.isAndroid) return; final androidInfo = await DeviceInfoPlugin().androidInfo; if (androidInfo.version.sdkInt >= 33) { // Android 13+ 写入公共Download目录无需特殊权限,用MediaStore即可 } else if (androidInfo.version.sdkInt >= 30) { // Android 11-12 申请全文件访问权限 final status = await Permission.manageExternalStorage.status; if (!status.isGranted) { await openAppSettings(); // 跳转到App设置页让用户手动开启 } } else { // Android 10及以下 申请传统存储权限 final status = await Permission.storage.status; if (!status.isGranted) { await Permission.storage.request(); } } }
3. 正确写入公共Download目录的两种方式
方式1:使用MediaStore API(推荐,无需全文件访问权限)
Android 10+推荐用MediaStore写入公共目录,符合官方存储规范,无需申请MANAGE_EXTERNAL_STORAGE:
import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:device_info_plus/device_info_plus.dart'; Future<void> saveFileToDownload(Uint8List bytes, String fileName) async { if (Platform.isAndroid) { final androidInfo = await DeviceInfoPlugin().androidInfo; if (androidInfo.version.sdkInt >= 29) { // 通过MethodChannel调用Android原生代码写入 const channel = MethodChannel('com.example.file_save'); try { await channel.invokeMethod('saveToDownload', { 'bytes': bytes, 'fileName': fileName, 'mimeType': 'application/pdf' // 根据文件类型调整 }); } on PlatformException catch (e) { print('保存失败:${e.message}'); } } else { // Android 9及以下直接写入 final dir = Directory('/storage/emulated/0/Download'); if (await dir.exists()) { await File('${dir.path}/$fileName').writeAsBytes(bytes); } } } else if (Platform.isIOS) { final dir = await getApplicationDocumentsDirectory(); await File('${dir.path}/$fileName').writeAsBytes(bytes); } }
Android原生代码(MainActivity.kt):
import android.content.ContentValues import android.os.Build 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.example.file_save" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "saveToDownload") { val bytes = call.argument<ByteArray>("bytes") val fileName = call.argument<String>("fileName") val mimeType = call.argument<String>("mimeType") if (bytes != null && fileName != null && mimeType != null) { saveToDownload(bytes, fileName, mimeType) result.success(null) } else { result.error("INVALID_ARGS", "参数缺失", null) } } else { result.notImplemented() } } } private fun saveToDownload(bytes: ByteArray, fileName: String, mimeType: String) { val values = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, mimeType) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Downloads.RELATIVE_PATH, "Download") put(MediaStore.Downloads.IS_PENDING, 1) } } contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)?.let { uri -> contentResolver.openOutputStream(uri)?.use { it.write(bytes) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.clear() values.put(MediaStore.Downloads.IS_PENDING, 0) contentResolver.update(uri, values, null, null) } } } }
方式2:使用全文件访问权限(仅需访问所有文件时使用)
若必须用直接路径写入,需确保用户已开启MANAGE_EXTERNAL_STORAGE权限:
Future<String?> getDownloadPath() async { if (Platform.isAndroid) { final hasManagePerm = await Permission.manageExternalStorage.isGranted; return hasManagePerm ? Directory('/storage/emulated/0/Download').path : null; } else if (Platform.isIOS) { return (await getApplicationDocumentsDirectory()).path; } return null; }
4. 修复代码中的语法错误
你的downLoadFile函数重复定义了bytes变量,会导致编译失败:
final bytes = response.bodyBytes; //... final bytes = result; // 重复定义,需删除或重命名
改为:
final bytes = response.bodyBytes; //... File(filePath).writeAsBytes(bytes); // 直接使用已定义的bytes变量
5. 其他注意事项
- 执行
flutter clean后重新构建项目,确保Manifest和gradle修改生效; - 优先用实体设备测试,部分模拟器存在存储权限模拟异常。
内容的提问来源于stack exchange,提问作者Shogin
相关产品推荐
相关产品推荐

