Android 13无法访问Download目录文件问题求助
解决Android 13下Flutter访问Download目录报错问题
问题根源
Android 13(API 33)对存储权限和访问逻辑做了核心调整:
READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE权限被废弃,公共存储访问受Scoped Storage严格限制,直接通过文件路径访问Download等公共目录会被拦截。getExternalStorageDirectory()自API 29起已被标记为废弃,Android 13下该方法返回的路径无法直接映射到真实的公共Download目录。
可行解决方案
方案1:用存储访问框架(SAF)访问Download目录(官方推荐)
SAF通过用户授权的方式访问公共存储,无需特殊权限,适配全版本:
import 'package:flutter/services.dart'; import 'dart:io'; // 调用原生方法打开Download目录选择文件 Future<File?> pickDownloadFile() async { const channel = MethodChannel('com.example.storage/saf'); try { final String? filePath = await channel.invokeMethod('pickDownloadFile'); return filePath != null ? File(filePath) : null; } on PlatformException catch (e) { print(e.message); return null; } }
对应的Android原生代码(MainActivity中):
class MainActivity : FlutterActivity() { private val CHANNEL = "com.example.storage/saf" private val REQUEST_PICK_FILE = 1001 override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "pickDownloadFile" -> { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "*/*" // 默认打开Download目录 val downloadUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadUri) } startActivityForResult(intent, REQUEST_PICK_FILE) result.success(null) } else -> result.notImplemented() } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_PICK_FILE && resultCode == RESULT_OK) { data?.data?.let { uri -> val filePath = getFilePathFromUri(uri) MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CHANNEL) .invokeMethod("filePicked", filePath) } } } private fun getFilePathFromUri(uri: Uri): String? { val projection = arrayOf(MediaStore.Downloads.DATA) contentResolver.query(uri, projection, null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { val index = cursor.getColumnIndexOrThrow(MediaStore.Downloads.DATA) return cursor.getString(index) } } return null } }
方案2:使用MANAGE_EXTERNAL_STORAGE权限(仅适合特殊场景)
如果必须通过文件路径直接访问,需完成以下步骤:
- 清单文件保留权限(添加tools忽略警告):
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
- 运行时引导用户到设置开启权限(该权限无法通过常规请求弹窗获取):
import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:io'; Future<bool> checkManageStoragePermission() async { var status = await Permission.manageExternalStorage.status; if (!status.isGranted) { status = await Permission.manageExternalStorage.request(); if (status.isDenied) { // 跳转到应用设置页面引导开启 await launchUrl(Uri.parse('package:${Platform.packageName}/settings')); } } return status.isGranted; }
- 修正路径获取逻辑:
import 'package:path_provider_ex/path_provider_ex.dart'; // 需要添加依赖 import 'dart:io'; Future<Directory?> getDownloadDir() async { if (Platform.isAndroid) { try { final storageDirs = await PathProviderEx.getStorageDirectories(); return Directory('${storageDirs.first.path}/Download'); } catch (e) { return null; } } else { return await getApplicationDocumentsDirectory(); } }
方案3:用MediaStore创建Download目录文件
针对创建文件到Download目录的场景,可通过MediaStore实现:
import 'package:flutter/services.dart'; import 'dart:typed_data'; Future<bool> saveToDownload(String fileName, Uint8List fileContent) async { const channel = MethodChannel('com.example.storage/mediastore'); try { return await channel.invokeMethod('saveToDownload', { 'fileName': fileName, 'content': fileContent, }); } on PlatformException catch (e) { print(e.message); return false; } }
Android原生代码:
class MainActivity : FlutterActivity() { private val CHANNEL = "com.example.storage/mediastore" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "saveToDownload" -> { val fileName = call.argument<String>("fileName") ?: run { result.error("INVALID_NAME", "文件名不能为空", null) return@setMethodCallHandler } val content = call.argument<ByteArray>("content") ?: run { result.error("INVALID_CONTENT", "文件内容不能为空", null) return@setMethodCallHandler } val values = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, "text/csv") put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) uri?.let { contentResolver.openOutputStream(it)?.use { stream -> stream.write(content) result.success(true) } ?: result.error("WRITE_FAILED", "无法打开输出流", null) } ?: result.error("INSERT_FAILED", "无法写入MediaStore", null) } else -> result.notImplemented() } } } }
核心注意事项
- 避免直接拼接
getExternalStorageDirectory()的路径访问公共目录,Android 13下Scoped Storage会拦截此类操作。 MANAGE_EXTERNAL_STORAGE权限仅适合文件管理器类应用,普通应用使用SAF或MediaStore更合规,避免被Google Play拒审。- 测试时需确认Android 13设备已授予对应权限(MANAGE_EXTERNAL_STORAGE需手动到设置开启)。
内容的提问来源于stack exchange,提问作者preeti
相关产品推荐
相关产品推荐

