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

Flutter:如何避免系统文件夹选择器重复请求权限?

Kotlin 原生解决方案

核心思路

基于Storage Access Framework(SAF)实现已授权文件夹免重复权限请求,关键是持久化保存Uri的访问权限,而非仅存储Uri字符串。同时需区分「打开已授权文件夹」和「重新选择新文件夹」两个操作入口,既保留权限记忆,又不限制用户更换文件夹的需求。

步骤1:处理文件夹选择后的权限持久化

用户通过SAF选择文件夹并确认后,调用takePersistableUriPermission获取持久化权限,同时将Uri存入SharedPreferences:

private val REQUEST_CODE_PICK_FOLDER = 1001

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE_PICK_FOLDER && resultCode == RESULT_OK) {
        data?.data?.let { uri ->
            // 获取持久化读写权限
            val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            contentResolver.takePersistableUriPermission(uri, takeFlags)
            
            // 保存Uri到SharedPreferences
            val prefs = getSharedPreferences("SAF_Prefs", MODE_PRIVATE)
            prefs.edit().putString("AUTHORIZED_FOLDER_URI", uri.toString()).apply()
            
            // 后续直接使用该Uri执行文件操作,无需重复请求权限
        }
    }
}

步骤2:区分操作逻辑

给用户提供两个操作选项,分别对应不同逻辑:

  • 打开已授权文件夹:读取保存的Uri,先检查权限有效性,有效则直接使用,无效则引导重新选择
  • 重新选择文件夹:直接启动SAF选择器,忽略已保存的Uri,走首次选择流程
// 打开已授权文件夹逻辑
fun openAuthorizedFolder() {
    val prefs = getSharedPreferences("SAF_Prefs", MODE_PRIVATE)
    val uriString = prefs.getString("AUTHORIZED_FOLDER_URI", null)
    
    uriString?.let {
        val uri = Uri.parse(it)
        // 检查权限是否仍有效
        val hasReadPerm = contentResolver.checkUriPermission(uri, android.os.Process.myPid(), android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED
        val hasWritePerm = contentResolver.checkUriPermission(uri, android.os.Process.myPid(), android.os.Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED
        
        if (hasReadPerm && hasWritePerm) {
            // 权限有效,直接操作文件
            handleFileOperations(uri)
        } else {
            // 权限失效,重新选择文件夹
            launchFolderPicker()
        }
    } ?: run {
        // 无已授权文件夹,直接打开选择器
        launchFolderPicker()
    }
}

// 启动SAF文件夹选择器
fun launchFolderPicker() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    startActivityForResult(intent, REQUEST_CODE_PICK_FOLDER)
}

关键注意点

  • 持久化权限会随应用卸载自动失效,无需手动清理
  • 不要存储文件夹路径,SAF的Uri是唯一且稳定的标识,路径可能因系统操作变化

Flutter 解决方案

Flutter端通过MethodChannel对接原生SAF逻辑,实现权限持久化和操作区分,避免直接操作SAF Uri的兼容性问题。

核心思路

通过MethodChannel暴露两个方法给Flutter层:获取已授权文件夹、选择新文件夹,原生层处理权限持久化和有效性检查。

步骤1:Flutter端代码

定义MethodChannel封装原生调用:

import 'package:flutter/services.dart';

class SafFolderManager {
  static const MethodChannel _channel = MethodChannel('saf_folder_channel');

  // 获取已授权文件夹Uri(权限有效则返回,否则返回null)
  static Future<String?> getAuthorizedFolder() async {
    try {
      return await _channel.invokeMethod('getAuthorizedFolder');
    } on PlatformException catch (e) {
      print("SAF Error: ${e.message}");
      return null;
    }
  }

  // 启动SAF选择新文件夹
  static Future<String?> pickNewFolder() async {
    try {
      return await _channel.invokeMethod('pickNewFolder');
    } on PlatformException catch (e) {
      print("SAF Error: ${e.message}");
      return null;
    }
  }
}

UI层提供操作入口:

ElevatedButton(
  onPressed: () async {
    String? uri = await SafFolderManager.getAuthorizedFolder();
    if (uri != null) {
      // 使用已授权文件夹Uri执行操作
    } else {
      // 无有效授权,引导用户选择新文件夹
      await SafFolderManager.pickNewFolder();
    }
  },
  child: const Text("打开已授权文件夹"),
),
ElevatedButton(
  onPressed: () async {
    String? uri = await SafFolderManager.pickNewFolder();
    if (uri != null) {
      // 保存Uri并执行文件操作
    }
  },
  child: const Text("重新选择文件夹"),
),

步骤2:原生端(Kotlin)MethodChannel实现

class MainActivity : FlutterActivity() {
    private val CHANNEL = "saf_folder_channel"
    private val REQUEST_CODE_PICK_FOLDER = 1001
    private var resultCallback: MethodChannel.Result? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            when (call.method) {
                "getAuthorizedFolder" -> {
                    val prefs = getSharedPreferences("SAF_Prefs", MODE_PRIVATE)
                    val uriString = prefs.getString("AUTHORIZED_FOLDER_URI", null)
                    
                    uriString?.let {
                        val uri = Uri.parse(it)
                        val hasReadPerm = checkUriPermission(uri, android.os.Process.myPid(), android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED
                        val hasWritePerm = checkUriPermission(uri, android.os.Process.myPid(), android.os.Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED
                        
                        if (hasReadPerm && hasWritePerm) {
                            result.success(uriString)
                        } else {
                            result.success(null)
                        }
                    } ?: result.success(null)
                }
                "pickNewFolder" -> {
                    resultCallback = result
                    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
                    startActivityForResult(intent, REQUEST_CODE_PICK_FOLDER)
                }
                else -> result.notImplemented()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE_PICK_FOLDER && resultCode == RESULT_OK) {
            data?.data?.let { uri ->
                val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                contentResolver.takePersistableUriPermission(uri, takeFlags)
                getSharedPreferences("SAF_Prefs", MODE_PRIVATE).edit().putString("AUTHORIZED_FOLDER_URI", uri.toString()).apply()
                resultCallback?.success(uri.toString())
            } ?: resultCallback?.success(null)
            resultCallback = null
        }
    }
}

关键注意点

  • Flutter层无法直接用File类操作SAF Uri,需通过原生层实现文件读写逻辑
  • AndroidManifest.xml无需声明传统存储权限,SAF已通过系统授权流程处理权限

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:12:27