1. Flutter 存储权限适配全解析
在移动应用开发中,文件存储权限一直是开发者需要重点处理的系统兼容性问题。特别是随着 Android 和 iOS 系统不断更新,各版本对存储权限的管理策略差异越来越大。作为 Flutter 开发者,我们需要一套能够覆盖主流系统版本的完整权限适配方案。
1.1 为什么存储权限如此复杂
存储权限的复杂性主要来自三个方面:
- 系统版本碎片化:Android 10 引入的分区存储(Scoped Storage)彻底改变了文件访问方式,而 iOS 14 开始的照片权限细分也增加了适配难度
- 权限类型多样化:从传统的 READ/WRITE_EXTERNAL_STORAGE 到 Android 13 的媒体细分权限,再到 iOS 的有限照片访问,权限类型呈爆炸式增长
- 审核政策严格化:Google Play 对 MANAGE_EXTERNAL_STORAGE 权限的严格限制,以及 App Store 对权限描述文案的审核要求,都增加了上架难度
提示:在实际项目中,建议建立系统版本矩阵表,明确各版本的特异行为,这是做好权限适配的基础。
1.2 适配方案设计思路
我们的适配方案需要遵循以下原则:
- 向下兼容:新版本系统的适配不能影响旧版本设备的正常运行
- 最小权限:只申请应用真正需要的权限,避免过度索权
- 优雅降级:当无法获取理想权限时,应有合理的备选方案
- 用户透明:权限申请时机合理,拒绝时提供明确引导
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Android 平台全版本适配
2.1 AndroidManifest 配置详解
Android 的权限配置需要根据不同的 SDK 版本进行差异化处理。以下是完整的 AndroidManifest.xml 配置示例:
xml复制<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- 基础存储权限(适配所有版本) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/> <!-- Android 10+ 自动失效 -->
<!-- Android 10 分区存储兼容模式 -->
<application
android:requestLegacyExternalStorage="true"
...>
</application>
<!-- Android 13+ 媒体细分权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<!-- Android 11+ 所有文件访问权限(慎用) -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- Android 14+ 视觉媒体选择权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
</manifest>
关键配置说明:
requestLegacyExternalStorage:仅对 Android 10 有效,保持传统存储访问方式maxSdkVersion="28":确保 WRITE_EXTERNAL_STORAGE 在 Android 10+ 不会被重复申请- 媒体细分权限:Android 13 开始需要按需申请照片、视频或音频权限
2.2 运行时权限申请逻辑
根据不同的 Android 版本,我们需要实现差异化的权限申请逻辑:
dart复制Future<bool> requestAndroidStoragePermission({
bool needAllFiles = false,
bool needPhotos = true,
bool needVideos = false,
bool needAudio = false,
}) async {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;
//
