Android如何兼容API21及以上版本正确保存文件至外部存储公共目录?
分版本处理的方式是正确的
因为MediaStore.Downloads.EXTERNAL_CONTENT_URI是Android 10(API29)才引入的常量,API29及以下版本无法直接使用该字段,同时API29之前官方允许通过Environment.getExternalStoragePublicDirectory操作公共存储目录,因此分版本适配是唯一可行的兼容方案。
以下是完整的API21+兼容实现:
1. 权限配置
首先在AndroidManifest.xml中声明权限,限制仅在API29及以下版本生效:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
- API23+版本需要动态申请
WRITE_EXTERNAL_STORAGE权限; - API30+版本无需申请该权限,MediaStore API默认支持写入公共下载目录。
2. 核心实现代码
import android.content.ContentValues import android.content.Context import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.core.content.FileProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream import java.io.IOException override suspend fun saveFile(byteArray: ByteArray, fileName: String): Uri? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { saveFileToMediaStore(byteArray, fileName) } else { saveFileToExternalStorage(byteArray, fileName) } } // Android 10+ 使用MediaStore API保存 private suspend fun saveFileToMediaStore(byteArray: ByteArray, fileName: String): Uri? { val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, fileName.getMimeType()) put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) put(MediaStore.Downloads.SIZE, byteArray.size.toLong()) } return withContext(Dispatchers.IO) { context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)?.also { uri -> context.contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write(byteArray) } } } } // Android 21-29 使用旧版公共存储API保存 private suspend fun saveFileToExternalStorage(byteArray: ByteArray, fileName: String): Uri? { return withContext(Dispatchers.IO) { val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) if (!downloadsDir.exists()) { downloadsDir.mkdirs() } val file = File(downloadsDir, fileName) return@withContext try { FileOutputStream(file).use { outputStream -> outputStream.write(byteArray) } // 适配Android 7.0+ 避免FileUriExposedException if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { getFileProviderUri(file) } else { Uri.fromFile(file) } } catch (e: IOException) { e.printStackTrace() null } } } // 适配Android 7.0+的FileProvider Uri生成 private fun getFileProviderUri(file: File): Uri { return FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", // 替换为你的App的FileProvider授权名 file ) } // 辅助方法:根据文件名获取MIME类型 private fun String.getMimeType(): String { val extension = this.substringAfterLast(".", "") return android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream" }
3. FileProvider配置(Android 7.0+ 必需)
在AndroidManifest.xml的<application>标签内添加FileProvider配置:
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>
然后在res/xml目录下创建file_paths.xml文件:
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="public_downloads" path="Download" /> </paths>
关键注意事项
- 所有文件操作必须在IO线程执行,代码中通过
withContext(Dispatchers.IO)确保; - API21-29版本必须先获取
WRITE_EXTERNAL_STORAGE权限才能写入文件; - MIME类型获取方法可根据实际需求调整,确保与文件类型匹配。
内容的提问来源于stack exchange,提问作者testivanivan
相关产品推荐
相关产品推荐

