Flutter:Android 33及以上版本如何将音频保存至下载文件夹
针对Android API 33+保存音频到下载文件夹的解决方案
无需申请WRITE_EXTERNAL_STORAGE或MANAGE_EXTERNAL_STORAGE这类高风险权限,Google Play政策允许通过系统API直接操作公共下载目录,以下是两种合规实现方案:
方案1:使用系统DownloadManager(推荐)
让Android系统负责下载和存储,无需权限,完全符合政策要求,还能自动管理下载进度和通知。
Flutter端代码
import 'package:flutter/services.dart'; const _downloadChannel = MethodChannel('com.yourapp/download_handler'); /// 调用系统DownloadManager下载音频到下载文件夹 Future<void> startSystemDownload(String audioUrl, String fileName) async { try { await _downloadChannel.invokeMethod('start_download', { 'url': audioUrl, 'file_name': fileName, }); } on PlatformException catch (e) { print('下载启动失败: ${e.message}'); } }
Android原生代码(MainActivity.kt)
import android.app.DownloadManager import android.content.Context import android.net.Uri import android.os.Environment import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { private val CHANNEL = "com.yourapp/download_handler" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "start_download") { val url = call.argument<String>("url") val fileName = call.argument<String>("file_name") if (url.isNullOrEmpty() || fileName.isNullOrEmpty()) { result.error("INVALID_PARAM", "URL或文件名不能为空", null) return@setMethodCallHandler } val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val request = DownloadManager.Request(Uri.parse(url)) .setTitle(fileName) .setDescription("音频文件下载中") .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) downloadManager.enqueue(request) result.success("下载已启动") } else { result.notImplemented() } } } }
方案2:手动写入音频数据到下载文件夹
如果已经自行下载了音频字节数据,可通过MediaStore API写入公共下载目录,无需特殊权限。
Flutter端代码
import 'dart:typed_data'; import 'package:flutter/services.dart'; const _saveChannel = MethodChannel('com.yourapp/save_audio'); /// 将音频字节数据保存到下载文件夹 Future<void> saveAudioBytesToDownloads(Uint8List audioBytes, String fileName) async { try { await _saveChannel.invokeMethod('save_audio_bytes', { 'bytes': audioBytes, 'file_name': fileName, }); } on PlatformException catch (e) { print('文件保存失败: ${e.message}'); } }
Android原生代码(MainActivity.kt)
import android.content.ContentValues import android.content.Context 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.OutputStream class MainActivity : FlutterActivity() { private val CHANNEL = "com.yourapp/save_audio" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "save_audio_bytes") { val audioBytes = call.argument<ByteArray>("bytes") val fileName = call.argument<String>("file_name") if (audioBytes == null || fileName.isNullOrEmpty()) { result.error("INVALID_PARAM", "音频数据或文件名不能为空", null) return@setMethodCallHandler } val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(MediaStore.MediaColumns.MIME_TYPE, "audio/mpeg") // 根据音频格式调整,如audio/ogg if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) put(MediaStore.MediaColumns.IS_PENDING, 1) } } val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues) if (uri == null) { result.error("INSERT_FAILED", "无法创建文件条目", null) return@setMethodCallHandler } try { contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write(audioBytes) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) contentResolver.update(uri, contentValues, null, null) } result.success("文件已保存至下载文件夹") } catch (e: Exception) { contentResolver.delete(uri, null, null) result.error("WRITE_FAILED", e.message, null) } } else { result.notImplemented() } } } }
关键注意事项
- 确保项目
targetSdkVersion设置为33或更高 - 无需在
AndroidManifest.xml中声明任何存储相关权限 - 优先使用方案1,更符合Google Play的权限规范,避免审核风险
内容的提问来源于stack exchange,提问作者touhid udoy
相关产品推荐
相关产品推荐

