Flutter获取Android可见Downloads路径存储文件的问题排查
解决Flutter应用下载文件到Android公共Download目录的问题
核心原因:Android 10+的Scoped Storage限制
你遇到的权限拒绝不是调试模式导致的,而是Android 10(API 29)及以上引入的Scoped Storage机制:
- 直接硬编码
/storage/emulated/0/Download/写入会被系统拦截,即使申请了WRITE_EXTERNAL_STORAGE权限 - API 30+开始,
WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE权限已被废弃,仅用于兼容旧版本
正确实现步骤
1. 配置AndroidManifest.xml
根据不同Android版本针对性配置:
<manifest ...> <!-- 兼容Android 9及以下版本 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- 仅作为临时兼容方案(不推荐长期使用):开启旧存储模式 --> <!-- <application android:requestLegacyExternalStorage="true" ...> --> <application ...> ... </application> </manifest>
2. 获取公共Download目录的正确方式
不要用硬编码路径或私有目录API,推荐以下两种方案:
方式一:使用path_provider获取公共目录(适配旧版本)
添加path_provider依赖后,通过官方API获取Download目录(Android 10+标记为废弃,但可配合临时兼容模式使用):
import 'package:path_provider/path_provider.dart'; Future<String> getPublicDownloadPath() async { final directory = await getExternalStoragePublicDirectory(DirectoryDownloads); return directory.path; }
方式二:用MediaStore写入(Android 10+推荐)
通过MediaStore创建可写入的公共文件,避免权限问题,步骤如下:
- 先下载文件到应用临时目录
- 通过MethodChannel调用Android原生代码,将文件写入公共Download目录
Dart端代码:
import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:flutter/services.dart'; Future<void> downloadToPublicDownload(String url, String fileName) async { final dio = Dio(); // 下载到应用临时目录 final tempDir = await getTemporaryDirectory(); final tempFilePath = path.join(tempDir.path, fileName); await dio.download(url, tempFilePath); // 调用原生方法保存到公共Download目录 final file = File(tempFilePath); final bytes = await file.readAsBytes(); final channel = MethodChannel('com.example.file_handler'); try { await channel.invokeMethod('saveToDownloads', { 'fileName': fileName, 'bytes': bytes, }); } on PlatformException catch (e) { print('保存失败:${e.message}'); } // 删除临时文件 await file.delete(); }
Android原生端(MainActivity.kt)代码:
import android.content.ContentValues import android.net.Uri 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_handler" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "saveToDownloads") { val fileName = call.argument<String>("fileName") val bytes = call.argument<ByteArray>("bytes") if (fileName == null || bytes == null) { result.error("INVALID_ARGS", "参数缺失", null) return@setMethodCallHandler } val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, getMimeType(fileName)) put(MediaStore.Downloads.RELATIVE_PATH, "Download/") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Downloads.IS_PENDING, 1) } } val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) if (uri != null) { contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write(bytes) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentValues.put(MediaStore.Downloads.IS_PENDING, 0) contentResolver.update(uri, contentValues, null, null) } result.success(true) } ?: run { result.error("WRITE_FAIL", "无法打开输出流", null) } } else { result.error("INSERT_FAIL", "无法创建公共文件", null) } } else { result.notImplemented() } } } private fun getMimeType(fileName: String): String { val extension = fileName.substringAfterLast('.', "") return when (extension.lowercase()) { "pdf" -> "application/pdf" "jpg", "jpeg" -> "image/jpeg" "png" -> "image/png" "txt" -> "text/plain" // 按需添加更多文件类型 else -> "application/octet-stream" } } }
3. 权限请求的正确逻辑
- Android 9及以下:需要请求
Permission.storage权限 - Android 10+:通过MediaStore写入公共Download目录无需额外权限
- Android 11+:仅批量操作文件时需要
MANAGE_EXTERNAL_STORAGE权限,普通下载不需要
权限请求代码示例:
import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; Future<bool> checkAndRequestPermissions() async { if (Platform.isAndroid) { final androidInfo = await DeviceInfoPlugin().androidInfo; if (androidInfo.version.sdkInt <= 28) { final status = await Permission.storage.request(); return status.isGranted; } } return true; }
常见误区澄清
- 调试模式不影响权限:实体平板调试和正常运行的权限逻辑一致,权限拒绝是存储策略导致的,和调试模式无关
- 不要硬编码路径:不同设备的公共存储路径可能存在差异,硬编码会引发兼容性问题
- 优先适配MediaStore:依赖废弃API仅作为临时方案,长期需遵循Android官方存储规范
内容的提问来源于stack exchange,提问作者markhorrocks
相关产品推荐
相关产品推荐

