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

Flutter如何获取Android公共Documents/Download文件夹存储权限?

解决Flutter Android保存文件到公共Download目录权限被拒问题

1. 修正权限配置与申请逻辑

  • AndroidManifest.xml 权限调整:确保权限声明适配不同Android版本,且requestLegacyExternalStorage配置正确:
    <!-- Android 10及以下所需的存储权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
    <!-- Android 11+ 写入公共Download目录无需WRITE_EXTERNAL_STORAGE,不要加MANAGE_EXTERNAL_STORAGE(仅特殊场景需要) -->
    
    <application
        ...
        android:requestLegacyExternalStorage="true">
    
  • 权限申请代码:针对permission_handler 9.2.0适配版本逻辑:
    import 'package:permission_handler/permission_handler.dart';
    
    Future<bool> requestStoragePermission() async {
      final status = await Permission.storage.request();
      if (status.isGranted) {
        return true;
      } else if (status.isPermanentlyDenied) {
        await openAppSettings();
        return false;
      }
      return false;
    }
    

2. 用MediaStore API替代硬编码路径

Scoped Storage下硬编码公共目录路径会触发权限拦截,推荐通过MediaStore直接写入,无需依赖私有路径:

Flutter端代码

import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';

Future<void> downloadToPublicDownload(String url, String fileName) async {
  final dio = Dio();
  try {
    // 下载文件到内存
    final response = await dio.get(url, options: Options(responseType: ResponseType.bytes));
    final bytes = response.data as Uint8List;

    // 通过MethodChannel调用原生代码写入Download目录
    const channel = MethodChannel('com.example.file_operations');
    await channel.invokeMethod('saveToDownload', {
      'fileName': fileName,
      'fileBytes': bytes,
    });
  } catch (e) {
    print('保存失败: $e');
  }
}

Android原生端实现

创建FileOperations.kt:

package com.example.your_app_package

import android.content.ContentValues
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.FileOutputStream

class FileOperations(private val engine: FlutterEngine) {
    private val CHANNEL = "com.example.file_operations"

    fun registerChannel() {
        MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            when (call.method) {
                "saveToDownload" -> {
                    val fileName = call.argument<String>("fileName")
                    val bytes = call.argument<ByteArray>("fileBytes")
                    if (fileName != null && bytes != null) {
                        saveToPublicDownload(fileName, bytes)
                        result.success(true)
                    } else {
                        result.error("INVALID_PARAMS", "文件名或字节数据为空", null)
                    }
                }
                else -> result.notImplemented()
            }
        }
    }

    private fun saveToPublicDownload(fileName: String, bytes: ByteArray) {
        val contentValues = ContentValues().apply {
            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            put(MediaStore.Downloads.MIME_TYPE, getMimeType(fileName))
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
                put(MediaStore.Downloads.IS_PENDING, 1)
            } else {
                val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                put(MediaStore.Downloads.DATA, "${downloadDir.absolutePath}/$fileName")
            }
        }

        val resolver = engine.context.contentResolver
        val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)

        uri?.let {
            resolver.openOutputStream(it)?.use { stream ->
                stream.write(bytes)
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                contentValues.clear()
                contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
                resolver.update(it, contentValues, null, null)
            }
        }
    }

    private fun getMimeType(fileName: String): String {
        return when {
            fileName.endsWith(".pdf") -> "application/pdf"
            fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
            fileName.endsWith(".png") -> "image/png"
            fileName.endsWith(".txt") -> "text/plain"
            else -> "application/octet-stream"
        }
    }
}

在MainActivity.kt中注册Channel:

package com.example.your_app_package

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        FileOperations(flutterEngine).registerChannel()
    }
}

3. 依赖与版本验证

  • 保持compileSdkVersion=32、targetSdkVersion=32,避免自动升级到API33导致权限逻辑冲突
  • permission_handler 9.2.0适配当前compileSdk,无需升级;dio和path_provider版本可保持现有配置

4. 测试注意事项

  • 卸载旧版本应用后重新安装,确保requestLegacyExternalStorage生效
  • Android 11+设备必须使用MediaStore方式写入公共目录,硬编码路径会被Scoped Storage拦截

内容的提问来源于stack exchange,提问作者markhorrocks

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 14:05:22