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

Jetpack Compose中如何用ViewModel替代CursorLoader加载相册及图片数据

实现方案(Jetpack Compose + ViewModel 加载相册数据)

1. 前置权限配置

首先在AndroidManifest.xml中添加对应存储权限:

<!-- Android 13及以上版本 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- Android 12及以下版本 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

2. 数据结构定义

你原有代码中的Image、Album类可直接复用,补充字段参考如下:

data class Image(
    val id: String,
    val name: String,
    val uri: String,
    val width: Float,
    val height: Float,
    val addTime: Long,
    val folderName: String // 用于归类相册
)

data class Album(
    val folderName: String,
    val coverUri: String,
    val imageCount: Int,
    val imageList: List<Image>
)

3. ViewModel逻辑实现

所有查询逻辑放在ViewModel中,用协程执行后台查询,避免阻塞主线程,同时自动管理生命周期避免内存泄漏:

import android.app.Application
import android.provider.MediaStore
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID

class AlbumViewModel(application: Application) : AndroidViewModel(application) {
    private val _albumList = MutableStateFlow<List<Album>>(emptyList())
    val albumList: StateFlow<List<Album>> = _albumList

    // 权限申请通过后调用该方法触发加载
    fun loadAlbumImages() {
        viewModelScope.launch {
            val albums = queryImagesFromMediaStore()
            _albumList.value = albums
        }
    }

    private suspend fun queryImagesFromMediaStore(): List<Album> = withContext(Dispatchers.IO) {
        val imageList = mutableListOf<Image>()
        val projection = arrayOf(
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DATA,
            MediaStore.Images.Media.WIDTH,
            MediaStore.Images.Media.HEIGHT,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.DATE_ADDED,
            MediaStore.Images.Media.BUCKET_DISPLAY_NAME // 系统直接返回所属相册名,无需自行拆分路径
        )

        // use方法自动关闭Cursor,避免内存泄漏
        getApplication<Application>().contentResolver.query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            projection,
            null,
            null,
            "${MediaStore.Images.Media.DATE_ADDED} DESC"
        )?.use { cursor ->
            if (cursor.count > 0) {
                cursor.moveToFirst()
                do {
                    val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
                    val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
                    val width = cursor.getFloat(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.WIDTH))
                    val height = cursor.getFloat(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.HEIGHT))
                    val time = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED))
                    val folderName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)) ?: "其他"

                    val image = Image(
                        id = UUID.randomUUID().toString(),
                        name = name,
                        uri = "file:///$path",
                        width = width,
                        height = height,
                        addTime = time,
                        folderName = folderName
                    )
                    imageList.add(image)
                } while (cursor.moveToNext())
            }
        }
        return@withContext splitFolder(imageList)
    }

    // 按文件夹分组生成相册列表,可直接复用你原有拆分逻辑
    private fun splitFolder(imageList: List<Image>): List<Album> {
        val albumMap = imageList.groupBy { it.folderName }
        return albumMap.map { entry ->
            Album(
                folderName = entry.key,
                coverUri = entry.value.first().uri,
                imageCount = entry.value.size,
                imageList = entry.value
            )
        }.toList()
    }
}

4. Compose 侧调用

在Compose页面中先申请存储权限,再监听数据变化渲染UI:

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsStateWithLifecycle
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun AlbumPickerScreen(
    albumViewModel: AlbumViewModel = viewModel(LocalContext.current as androidx.activity.ComponentActivity)
) {
    val albumList = albumViewModel.albumList.collectAsStateWithLifecycle().value
    // 权限申请器
    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = { granted ->
            if (granted) albumViewModel.loadAlbumImages()
        }
    )

    // 进入页面自动申请权限
    LaunchedEffect(key1 = Unit) {
        val permission = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
            android.Manifest.permission.READ_MEDIA_IMAGES
        } else {
            android.Manifest.permission.READ_EXTERNAL_STORAGE
        }
        permissionLauncher.launch(permission)
    }

    // 自定义渲染相册/图片列表,实现选图逻辑即可
    LazyVerticalGrid(columns = GridCells.Fixed(3)) {
        items(albumList.flatMap { it.imageList }) { image ->
            // 可配合Coil/Glide加载图片
            Text(text = image.name)
        }
    }
}

内容的提问来源于stack exchange,提问作者gaohomway

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 14:48:05