Flutter应用使用Media Store权限在手机下载文件夹保存PDF以替代MANAGE_EXTERNAL_STORAGE权限问题求助
Flutter应用使用Media Store权限在手机下载文件夹保存PDF以替代MANAGE_EXTERNAL_STORAGE权限问题求助
兄弟,太懂你这种被Google Play拒审的糟心了!我之前做Flutter项目的时候,就是因为用了MANAGE_EXTERNAL_STORAGE权限被打回来两次,后来换成Media Store就顺利过审了,给你唠唠具体怎么弄:
先搞懂为什么MANAGE_EXTERNAL_STORAGE会被拒
Google现在对这个权限卡得特别严,它只开放给真正的文件管理器类应用,普通App(比如你这种只是存PDF到下载目录的)用这个权限,审核100%会被拒——因为违反了Android 10之后强制推行的Scoped Storage规范,这个权限的权限范围太大,不符合隐私保护要求。
具体实现步骤
1. 先修改AndroidManifest.xml
把之前加的<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />删掉,换成针对低版本设备的权限配置:
<!-- 仅Android 9及以下版本需要这个权限,API 29+无需申请 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- 可选:如果需要读取App缓存里的PDF文件,添加这个权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
另外要确保你的App的targetSdkVersion设置成30及以上(在android/app/build.gradle文件里配置),这是Scoped Storage的强制要求。
2. 通过MethodChannel调用Android原生代码实现保存
Flutter官方没有直接封装Media Store的完整API,所以我们需要写点Android原生代码,通过MethodChannel和Flutter端通信,完成PDF保存逻辑。
第一步:编写Android原生的保存逻辑
打开android/app/src/main/kotlin/[你的包名]/MainActivity.kt,添加MethodChannel的处理逻辑:
import android.content.ContentValues import android.net.Uri import android.os.Build import android.provider.MediaStore import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import java.io.File import java.io.FileInputStream import java.io.OutputStream class MainActivity : FlutterActivity() { // 替换成你自己的Channel标识,要和Flutter端保持一致 private val CHANNEL = "com.your.app/pdf_saver" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "savePdfToDownloads") { val pdfPath = call.argument<String>("pdfPath") val fileName = call.argument<String>("fileName") if (pdfPath == null || fileName == null) { result.error("INVALID_ARGS", "PDF路径或文件名不能为空", null) return@setMethodCallHandler } val saveSuccess = savePdfToDownloads(pdfPath, fileName) if (saveSuccess) { result.success("PDF已成功保存到下载目录") } else { result.error("SAVE_FAILED", "保存失败,请检查文件是否存在或权限配置", null) } } else { result.notImplemented() } } } private fun savePdfToDownloads(pdfPath: String, fileName: String): Boolean { val pdfFile = File(pdfPath) if (!pdfFile.exists()) return false val contentValues = ContentValues().apply { // 设置文件名和MIME类型 put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, "application/pdf") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 可选:在下载目录下创建App专属子文件夹,方便用户查找 put(MediaStore.Downloads.RELATIVE_PATH, "Download/我的PDF文件") // 标记文件为待完成状态,避免系统提前扫描 put(MediaStore.Downloads.IS_PENDING, 1) } } val resolver = contentResolver // 向MediaStore插入一条下载目录的文件记录 val uri: Uri? = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) if (uri == null) return false return try { // 获取输出流并写入PDF数据 resolver.openOutputStream(uri)?.use { outputStream -> FileInputStream(pdfFile).use { inputStream -> inputStream.copyTo(outputStream) } } // Android 10+需要更新状态为已完成 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.Downloads.IS_PENDING, 0) resolver.update(uri, contentValues, null, null) } true } catch (e: Exception) { e.printStackTrace() // 保存失败,删除已插入的空记录 resolver.delete(uri, null, null) false } } }
第二步:Flutter端调用原生方法
在Flutter代码里创建MethodChannel,调用原生的保存逻辑:
import 'package:flutter/services.dart'; class PdfSaver { // 和原生端的Channel标识保持完全一致 static const MethodChannel _channel = MethodChannel('com.your.app/pdf_saver'); static Future<String?> savePdfToDownloads(String pdfPath, String fileName) async { try { final String result = await _channel.invokeMethod( 'savePdfToDownloads', {'pdfPath': pdfPath, 'fileName': fileName}, ); return result; } on PlatformException catch (e) { return '保存失败:${e.message}'; } } }
之后在需要保存PDF的地方直接调用即可:
// 示例:保存缓存里生成的PDF文件 final pdfLocalPath = '/data/user/0/com.your.app/cache/生成的文件.pdf'; final saveResult = await PdfSaver.savePdfToDownloads( pdfLocalPath, '我的文档_${DateTime.now().millisecond}.pdf', // 加时间戳避免重名覆盖 ); print(saveResult);
额外注意事项
- 文件名唯一性:最好给文件名加上时间戳、用户ID等标识,避免覆盖下载目录里的同名文件
- 低版本权限处理:针对Android 9及以下设备,需要提前用
permission_handler包请求WRITE_EXTERNAL_STORAGE权限 - 审核说明:提交到Google Play时,一定要在权限声明里写清楚使用场景,比如“允许用户保存生成的PDF文档到下载目录,方便查看、分享或打印”,帮助审核团队理解你的需求
内容来源于stack exchange




