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
相关产品推荐
相关产品推荐

