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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 03:42:20