Android Kotlin如何将文件保存到公共存储?
解决API30+写入公共Documents目录的问题
核心原因
Android 11(API30)起启用分区存储机制,直接通过File API写入公共共享目录(如/storage/emulated/0/Documents)会被系统禁止,即使在Manifest中声明WRITE_EXTERNAL_STORAGE权限也无效。
方案1:使用MediaStore写入公共Documents目录
这种方式无需额外权限(API30+无需申请存储权限),系统自动处理文件存储授权,适合直接写入指定公共目录的场景。以下是Kotlin实现代码:
工具类代码
import android.content.ContentValues import android.content.Context import android.net.Uri import android.os.Build import android.provider.MediaStore fun appendTextToPublicDocument(context: Context, fileName: String, content: String) { val contentValues = ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, fileName) put(MediaStore.Downloads.MIME_TYPE, "text/plain") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 指定Documents下的AltiPro子目录,不存在会自动创建 put(MediaStore.Downloads.RELATIVE_PATH, "Documents/AltiPro") put(MediaStore.Downloads.IS_PENDING, 1) } } val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) uri?.let { try { context.contentResolver.openOutputStream(it).use { outputStream -> outputStream?.let { stream -> // 读取已有内容并追加新内容(API30+支持直接读取) val existingContent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { context.contentResolver.openInputStream(it)?.bufferedReader()?.readText() ?: "" } else { // API29及以下可补充旧逻辑,此处优先适配API30+ "" } stream.write((existingContent + content).toByteArray()) } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.Downloads.IS_PENDING, 0) context.contentResolver.update(it, contentValues, null, null) } } catch (e: Exception) { e.printStackTrace() // 可在此添加写入失败的提示逻辑 } } }
使用示例
在Activity/Fragment中直接调用:
val fileName = "AltiPro${System.currentTimeMillis()}.txt" val content = "${location.time},${location.latitude},${location.longitude},$AltitudeInt\n" appendTextToPublicDocument(this, fileName, content)
方案2:存储访问框架(SAF)让用户选择存储位置
如果需要让用户自行指定保存目录,可使用SAF实现,步骤如下:
1. 启动文件选择器
import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import android.provider.DocumentsContract class MainActivity : AppCompatActivity() { private val CREATE_FILE_REQUEST = 1001 fun launchFileCreator() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "text/plain" putExtra(Intent.EXTRA_TITLE, "AltiPro${System.currentTimeMillis()}.txt") // 默认打开Documents目录 putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } startActivityForResult(intent, CREATE_FILE_REQUEST) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == CREATE_FILE_REQUEST && resultCode == RESULT_OK) { data?.data?.let { uri -> // 保存Uri到SharedPreferences,方便后续追加内容 getSharedPreferences("AppPrefs", MODE_PRIVATE).edit() .putString("saved_file_uri", uri.toString()) .apply() // 写入初始内容 appendTextToUri(this, uri, "${location.time},${location.latitude},${location.longitude},$AltitudeInt\n") } } } }
2. 追加内容到选中文件
fun appendTextToUri(context: Context, uri: Uri, content: String) { try { // "wa"模式表示追加写入 context.contentResolver.openOutputStream(uri, "wa")?.use { outputStream -> outputStream.write(content.toByteArray()) } } catch (e: Exception) { e.printStackTrace() } }
注意事项
- 方案1生成的文件会出现在系统「文件」应用的
Documents/AltiPro目录下,用户可直接分享至Excel或Google Earth。 - 若需支持API29及以下版本,需在Manifest中添加
WRITE_EXTERNAL_STORAGE权限,并在运行时申请权限。
内容的提问来源于stack exchange,提问作者Jerome Jeanneret
相关产品推荐
相关产品推荐

