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

Android如何基于MVVM实现图库多图上传至RecyclerView展示

MVVM架构实现多图选择+RecyclerView展示+上传方案

原有非MVVM实现的核心问题是业务逻辑、数据状态全耦合在Activity/Fragment中,后续迭代加功能(比如上传进度、选图校验、图片编辑)很容易出现状态不一致、内存泄漏问题,按下面的分层逻辑重构即可,每层职责边界卡死,不会出现逻辑混乱。

分层职责定义

  • View层:仅承担UI渲染和交互转发,包含Activity/Fragment、RecyclerView Adapter、自定义View。所有用户操作(点击选图、点击删除图片、重试上传)直接调用ViewModel对应方法,不持有任何业务状态、不写业务判断逻辑,仅观察ViewModel的状态变化刷新UI。
  • ViewModel层:是页面所有UI状态的唯一可信源,负责处理选图结果、图片列表增删、上传状态流转、业务规则校验(比如选图数量上限),绝对不能持有Activity、View、Adapter这类短生命周期对象的引用,所有状态用生命周期感知的可观察容器持有。
  • Model层:承担纯数据能力,包含图片上传的网络请求封装、本地文件读取能力,不感知任何UI逻辑,处理结果通过回调返回给ViewModel。

落地实现步骤

1. 定义状态实体

先把所有UI需要用到的状态封装成不可变数据类,避免零散状态维护导致的不一致:

// 单张选中图片的状态
data class SelectedImageItem(
    val imageUri: Uri,
    val uploadProgress: Int = 0,
    val isUploaded: Boolean = false,
    val errorTip: String? = null
)

// 页面整体状态
data class ImagePickPageState(
    val selectedList: List<SelectedImageItem> = emptyList(),
    val maxSelectCount: Int = 9,
    val isUploading: Boolean = false
)

2. 实现ViewModel逻辑

用StateFlow(Java项目用LiveData即可)持有状态,所有状态修改逻辑收口在ViewModel内部:

class ImagePickViewModel : ViewModel() {
    private val _pageState = MutableStateFlow(ImagePickPageState())
    // 给View层暴露只读状态,禁止View层直接修改
    val pageState: StateFlow<ImagePickPageState> = _pageState.asStateFlow()

    // 接收选图结果,更新列表
    fun onImagesPicked(uris: List<Uri>) {
        val currentList = _pageState.value.selectedList.toMutableList()
        // 自动去重,避免同一张图重复添加
        val existUriSet = currentList.map { it.imageUri }.toSet()
        val newItems = uris.filterNot { it in existUriSet }
            .map { SelectedImageItem(imageUri = it) }
        
        // 校验选图数量上限
        val remainCount = _pageState.value.maxSelectCount - currentList.size
        val finalList = if (newItems.size > remainCount) {
            currentList + newItems.take(remainCount)
        } else {
            currentList + newItems
        }
        _pageState.update { it.copy(selectedList = finalList) }
        // 新加入的图片自动触发上传
        triggerUpload(newItems.map { it.imageUri })
    }

    // 删除指定位置的图片
    fun onDeleteImage(position: Int) {
        val currentList = _pageState.value.selectedList.toMutableList()
        if (position in currentList.indices) {
            // 可在这里加逻辑:如果图片正在上传,取消对应上传任务
            currentList.removeAt(position)
            _pageState.update { it.copy(selectedList = currentList) }
        }
    }

    // 更新单张图的上传状态
    fun updateUploadState(uri: Uri, progress: Int, isSuccess: Boolean, errorMsg: String? = null) {
        val currentList = _pageState.value.selectedList.toMutableList()
        val targetIndex = currentList.indexOfFirst { it.imageUri == uri }
        if (targetIndex != -1) {
            currentList[targetIndex] = currentList[targetIndex].copy(
                uploadProgress = progress,
                isUploaded = isSuccess,
                errorTip = errorMsg
            )
            _pageState.update { it.copy(selectedList = currentList) }
        }
    }

    // 调用Model层触发上传
    private fun triggerUpload(uris: List<Uri>) {
        // 通过viewModelScope启动协程调用Repository的上传方法,上传进度、结果回调到updateUploadState即可
        // 页面销毁时协程自动取消,不会造成内存泄漏
    }
}

3. View层实现

View层只做初始化、事件转发、状态渲染,不写任何业务判断:

class ImagePickActivity : AppCompatActivity() {
    // 通过ktx扩展获取ViewModel,不要自己new实例
    private val viewModel: ImagePickViewModel by viewModels()
    private lateinit var imageAdapter: SelectedImageAdapter
    // 注册系统多图选图Launcher,必须在onCreate阶段注册
    private val pickLauncher = registerForActivityResult(
        ActivityResultContracts.PickMultipleVisualMedia(9)
    ) { resultUris ->
        if (resultUris.isNotEmpty()) {
            // 选图结果直接转给ViewModel,View层不做任何处理
            viewModel.onImagesPicked(resultUris)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_image_pick)
        initList()
        initListener()
        // 生命周期感知的状态观察,页面退到后台自动停止收集,避免无效刷新
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.pageState.collect { state ->
                    imageAdapter.submitList(state.selectedList)
                    // 选满上限后禁用选图按钮
                    btnPick.isEnabled = state.selectedList.size < state.maxSelectCount
                }
            }
        }
    }

    private fun initList() {
        imageAdapter = SelectedImageAdapter(
            // 删除点击事件直接转发给ViewModel,Adapter和Activity都不做列表删除逻辑
            onDeleteClick = { position -> viewModel.onDeleteImage(position) }
        )
        rvImageList.adapter = imageAdapter
        rvImageList.layoutManager = GridLayoutManager(this, 3)
    }

    private fun initListener() {
        btnPick.setOnClickListener {
            pickLauncher.launch(
                PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
            )
        }
    }
}

Adapter只做纯视图绑定:用图片加载框架加载Uri、根据状态更新进度条、显示失败提示即可,不要在Adapter里持有可变列表做数据修改。

4. Model层实现

封装ImageRepository类,内部实现图片上传逻辑:

  • 接收Uri列表,通过ContentResolver读取文件流
  • 实现上传进度监听,把进度、成功/失败结果回调给ViewModel
  • 不要在Repository里持有任何UI相关引用,纯做数据处理

常见坑点

  • 禁止在ViewModel里传入Activity、View、Adapter实例,需要Context的场景用Application上下文,或者继承AndroidViewModel获取Application。
  • 所有列表数据的修改必须走ViewModel,View层不要自己维护一份列表副本,否则很容易出现数据和UI不一致的问题。
  • 上传任务必须关联viewModelScope,页面销毁时自动取消,避免内存泄漏和无意义的网络请求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:45:35