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
相关产品推荐
相关产品推荐

