You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 03:08:21