如何在Jetpack Compose中实现图片下载至本地存储?
Jetpack Compose 实现图片下载到本地存储的方案
1. 先搞定权限配置
存储权限是基础,不同Android版本的权限规则不一样,先在AndroidManifest.xml里加对应权限:
<!-- Android 10及以下需要的写入权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> <!-- Android 13+ 针对图片媒体的权限 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
在Compose里用rememberLauncherForActivityResult做动态权限申请,避免直接操作文件路径触发权限问题。
2. 正确用ContentResolver/MediaStore保存图片
你之前用ContentResolver报错,大概率是没按分区存储规则来操作,下面是完整可运行的实现:
第一步:写保存图片的核心函数
suspend fun saveImageToGallery( context: Context, bitmap: Bitmap, displayName: String = "compose_img_${System.currentTimeMillis()}.png", mimeType: String = "image/png" ): Uri? = withContext(Dispatchers.IO) { val contentValues = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, displayName) put(MediaStore.Images.Media.MIME_TYPE, mimeType) // 不同版本的存储路径配置 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) } else { put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/ComposeDownloads") } } var uri: Uri? = null try { // 先向MediaStore插入记录,拿到Uri再写入流 uri = context.contentResolver.insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues ) uri?.let { context.contentResolver.openOutputStream(it)?.use { outputStream -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) } } } catch (e: Exception) { e.printStackTrace() // 写入失败就删掉空记录 uri?.let { context.contentResolver.delete(it, null, null) } uri = null } uri }
第二步:在Compose页面调用(含权限判断)
@Composable fun ImageSaveDemo() { val context = LocalContext.current // 替换成你实际要保存的Bitmap(比如从网络加载的图片) val targetBitmap = remember { BitmapFactory.decodeResource(context.resources, R.drawable.your_image) } // 权限申请 launcher val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { CoroutineScope(Dispatchers.Main).launch { val saveUri = saveImageToGallery(context, targetBitmap) Toast.makeText(context, if (saveUri != null) "保存成功" else "保存失败", Toast.LENGTH_SHORT).show() } } else { Toast.makeText(context, "需要存储权限才能保存图片", Toast.LENGTH_SHORT).show() } } Button(onClick = { // 根据系统版本判断要申请的权限 val requiredPermission = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> Manifest.permission.READ_MEDIA_IMAGES Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Manifest.permission.READ_EXTERNAL_STORAGE else -> Manifest.permission.WRITE_EXTERNAL_STORAGE } if (ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED) { // 权限已通过,直接保存 CoroutineScope(Dispatchers.Main).launch { val saveUri = saveImageToGallery(context, targetBitmap) Toast.makeText(context, if (saveUri != null) "保存成功" else "保存失败", Toast.LENGTH_SHORT).show() } } else { // 申请权限 permissionLauncher.launch(requiredPermission) } }) { Text("保存图片到相册") } }
3. 常见报错排查
- SecurityException: Permission denied:检查权限是否正确申请,Android 10+别直接写文件路径,必须走MediaStore。
- IOException: Operation not permitted:确认
openOutputStream时流没有泄漏,ContentValues里的MIME类型要和图片格式匹配。 - 保存后找不到图片:调用
MediaScannerConnection通知系统刷新媒体库:MediaScannerConnection.scanFile( context, arrayOf(saveUri?.path), arrayOf(mimeType) ) { _, _ -> /* 扫描完成后的逻辑 */ }
内容的提问来源于stack exchange,提问作者NikAn
相关产品推荐
相关产品推荐

