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

Flutter存储至Download目录遇权限拒绝错误(errno=13)求助

解决Android公共Download目录写入权限问题及Permission-Handler日志报错

1. 解决Permission-Handler的「权限未在清单中找到」报错

日志中的[]22对应Permission.manageExternalStorage,出现该提示的核心原因:

  • MANAGE_EXTERNAL_STORAGE是Android 11(API 30)及以上的特殊权限,需确保App的targetSdkVersion≥30,且权限声明需适配版本。

修正步骤:

  1. 修改android/app/build.gradle,确保targetSdkVersion不低于30:
android {
    defaultConfig {
        targetSdkVersion 33 // 推荐使用最新稳定版本
    }
}
  1. 调整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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 18:01:02