简述

你提供的这套 Flutter 存储权限申请代码和配置,核心逻辑是对的,但缺少关键细节、异常处理、全场景适配(比如 Android 11/12 的特殊逻辑、iOS 细分权限、权限组申请等),我会补充完整的细节,让代码更健壮、适配所有主流系统版本(Android 10-14、iOS 11-17)。


一、补充 pubspec.yaml 依赖(完整)

除了 permission_handler,device_info_plus 是代码中依赖的(判断 Android 版本),需要一起添加,同时指定稳定版本:

1
2
3
4
5
6
dependencies:
flutter:
sdk: flutter
permission_handler: ^11.3.1 # 最新稳定版,修复了Android 14适配问题
device_info_plus: ^10.1.0 # 必须添加,用于判断Android SDK版本
path_provider: ^2.1.2 # 可选,配合权限获取文件路径(常用)

添加后执行:

flutter pub get

Android 配置

  1. AndroidManifest.xml 完整配置(分场景)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- 基础配置:适配所有Android版本 -->
    <manifest ...>
    <!-- 1. 兼容 Android 10 分区存储(可选,关闭Scoped Storage) -->
    <!-- 如果需要"访问所有文件",建议保留;仅访问媒体文件可删除 -->
    <application
    ...
    android:requestLegacyExternalStorage="true"> <!-- Android 10 适配 -->

    </application>

    <!-- 2. 媒体文件权限(基础,所有版本都加) -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28"/> <!-- Android 10+ 自动失效 -->

    <!-- 3. 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"/>

    <!-- 4. Android 11+ 访问所有文件(仅当需要读写非媒体文件时添加) -->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    </manifest>

关键补充说明:

  • android:requestLegacyExternalStorage=”true”:仅对 Android 10(SDK 29)生效,关闭分区存储,兼容旧版权限逻辑;Android 11+ 需用 MANAGE_EXTERNAL_STORAGE。

  • WRITE_EXTERNAL_STORAGE 加 maxSdkVersion=”28”:避免 Android 10+ 重复申请(系统自动忽略)。

  • MANAGE_EXTERNAL_STORAGE 是特殊权限:无法通过 permission_handler 直接申请,必须引导用户到系统设置页开启(下面代码会补充)。

  1. Android 14 额外适配(targetSdk 34)

如果你的 build.gradle 中 targetSdkVersion = 34,需添加:

1
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>

Ios 配置

  1. Info.plist 完整配置(覆盖全场景)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <dict>
    <!-- 相册读取(基础) -->
    <key>NSPhotoLibraryUsageDescription</key>
    <string>需要访问相册以选择图片/视频</string>

    <!-- 相册写入(iOS 11+) -->
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>需要将图片/视频保存到相册</string>

    <!-- iOS 14+ 选择性相册权限(用户可只授权部分照片) -->
    <key>NSPhotoLibraryLimitedUsageDescription</key>
    <string>需要访问您选择的照片/视频</string>

    <!-- 本地文件访问(如Documents目录,iOS 11+) -->
    <key>NSDocumentsFolderUsageDescription</key>
    <string>需要访问本地文件以管理您的资料</string>

    <!-- 可选:如果需要访问iCloud文件 -->
    <key>NSUbiquitousContainersUsageDescription</key>
    <string>需要访问iCloud文件以同步您的资料</string>
    </dict>

关键补充:

  • NSPhotoLibraryLimitedUsageDescription:iOS 14+ 新增,用户点击“选择照片”时显示,必须添加,否则权限申请会失败。

  • 描述文案不能为空/太简单:App Store 审核会拒绝(比如不能只写“访问相册”,要说明用途)。

权限管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import 'dart:io';

import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/services.dart'; // 用于捕获平台异常

/// 申请存储/媒体权限(支持全平台、全版本)
/// [needAllFiles]:是否需要访问"所有文件"(仅Android 11+生效)
Future<bool> requestStoragePermission({bool needAllFiles = false}) async {
try {
// ========== 1. 区分平台和版本 ==========
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;

// Android 13+(SDK 33+):申请媒体细分权限
if (sdkInt >= 33) {
// 按需申请:比如只需要图片+视频,就申请这两个
final photoStatus = await Permission.photos.request();
final videoStatus = await Permission.videos.request();
final audioStatus = await Permission.audio.request();
// 只要需要的权限都授权,就返回true
return photoStatus.isGranted && videoStatus.isGranted && audioStatus.isGranted;
}

// Android 11-12(SDK 30-32):申请存储权限 + 可选"所有文件"
else if (sdkInt >= 30) {
// 先申请基础存储权限
final storageStatus = await Permission.storage.request();
if (!storageStatus.isGranted) return false;

// 如果需要访问所有文件,引导到设置页
if (needAllFiles) {
final hasManagePerm = await Permission.manageExternalStorage.isGranted;
if (!hasManagePerm) {
await openAppSettings(); // 跳转到系统设置页
// 再次检查权限
return await Permission.manageExternalStorage.isGranted;
}
}
return true;
}

// Android 10-(SDK 29-):传统存储权限
else {
final status = await Permission.storage.request();
return status.isGranted;
}
}

// ========== 2. iOS 适配 ==========
else if (Platform.isIOS) {
// iOS 14+ 区分"全部"和"所选",统一申请photos权限即可
final status = await Permission.photos.request();
if (status.isGranted || status.isLimited) { // Limited是iOS 14+的"所选照片"权限
return true;
} else if (status.isPermanentlyDenied) {
await openAppSettings(); // 引导到设置页
return await Permission.photos.isGranted;
}
return false;
}

// 其他平台(如Linux/Windows)默认返回true
return true;
} on PlatformException catch (e) {
// 捕获平台异常(比如权限插件未初始化)
print("申请权限失败:${e.message}");
return false;
} catch (e) {
print("申请权限异常:$e");
return false;
}
}

/// 快速检查存储权限是否已授权
Future<bool> checkStoragePermission({bool needAllFiles = false}) async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;

if (sdkInt >= 33) {
return await Permission.photos.isGranted &&
await Permission.videos.isGranted &&
await Permission.audio.isGranted;
} else if (sdkInt >= 30 && needAllFiles) {
return await Permission.manageExternalStorage.isGranted;
} else {
return await Permission.storage.isGranted;
}
} else if (Platform.isIOS) {
final status = await Permission.photos.status;
return status.isGranted || status.isLimited;
}
return true;
}

代码关键补充点:

  1. 异常捕获:添加 try-catch 捕获 PlatformException(插件调用异常),避免崩溃。

  2. Android 11+ 所有文件权限:MANAGE_EXTERNAL_STORAGE 是特殊权限,必须引导用户到设置页开启,无法直接申请。

  3. iOS 14+ 有限权限:处理 isLimited 状态(用户只授权部分照片),这是 iOS 14+ 新增的权限类型。

  4. 权限组申请:Android 13+ 拆分了媒体权限(照片/视频/音频),按需申请更合规。

  5. 工具方法:新增 checkStoragePermission,方便提前检查权限状态。

五、使用示例(完整流程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 点击按钮申请权限
void onTapRequestPermission() async {
// 1. 先检查权限
final hasPerm = await checkStoragePermission(needAllFiles: true);
if (hasPerm) {
print("权限已授权,可访问文件");
return;
}

// 2. 申请权限
final success = await requestStoragePermission(needAllFiles: true);
if (success) {
print("权限申请成功");
// 执行文件操作(如读取相册、读写文件)
} else {
print("权限申请失败,请手动开启");
}
}

避坑指南

  1. Android 11+ MANAGE_EXTERNAL_STORAGE 审核:
  • Google Play 审核严格,仅允许文件管理器、备份类 App 使用该权限,普通 App 申请会被拒;

  • 优先使用分区存储(Scoped Storage),仅在必要时申请该权限。

  1. iOS 权限描述文案:
  • 必须真实描述用途,不能夸大(比如“访问相册用于展示头像”,而不是“访问相册”);

  • 缺少文案会导致 App 崩溃。

  1. 权限申请时机:
  • 不要一启动 App 就申请,要在用户触发操作时(比如点击“选择照片”)再申请,提升授权率。
  1. targetSdkVersion 适配:
  • 建议设置为 34(Android 14),并测试所有权限逻辑,避免版本兼容问题。

总结

  1. 配置层面:Android 需区分 SDK 版本添加权限,iOS 必须补充完整的权限描述文案;

  2. 代码层面:核心是处理 Android 13+ 媒体细分权限、Android 11+ 所有文件权限、iOS 14+ 有限相册权限;

  3. 体验层面:权限申请要在用户触发操作时进行,拒绝后引导到设置页,同时做好异常捕获。