You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动