MVVM架构下结合LiveData、协程、RecyclerView Adapter加载歌曲列表的实现疑问
MVVM 架构结合 LiveData 与协程的歌曲列表加载实现
现有代码的核心设计偏差
getSongs()方法职责完全错位:把网络请求、LiveData 赋值、返回 LiveData 三个逻辑揉进同一个 suspend 方法,既不符合 LiveData 的设计初衷,也会因为每次调用方法都重新执行接口请求,导致重复发起网络请求的问题- 线程调度逻辑冗余:Activity 层手动开启 IO 协程、手动切回主线程更新 UI,完全绕过了 LiveData 自动感知生命周期、自动在主线程分发数据的核心优势
- 适配器初始化逻辑不合理:每次拿到数据就新建适配器实例,会导致 RecyclerView 丢失原有滚动状态,也存在不必要的性能开销
实现思路拆解
按照 MVVM 单向数据流的职责隔离原则,各层的边界完全拆分,不需要额外拆分重复的方法:
- Repository 层:仅负责实际的数据获取逻辑(即调用 REST 接口拉取歌曲列表),方法本身为 suspend 类型,由调度器控制在后台线程运行,不持有任何 UI 相关引用
- ViewModel 层:持有可被 UI 观察的 LiveData 对象,对外仅暴露不可变的 LiveData 供 UI 观察,内部负责触发数据请求、把拉取到的数据更新到 LiveData 上
- Activity 层:仅负责初始化 RecyclerView、观察对应 LiveData 的数据变化,在数据更新时刷新适配器内容,完全不需要手动控制协程的线程切换逻辑。
修正后的参考代码
SongListActivityViewModel
class SongListActivityViewModel : ViewModel() { // 对外暴露不可变的 LiveData,避免 UI 层随意修改数据源 val songListLiveData: LiveData<List<Song>> = MutableLiveData() init { // 页面初始化时自动触发数据加载,不需要 Activity 层手动控制 loadSongs() } private fun loadSongs() { // 使用 viewModelScope 启动协程,替代手动创建的 CoroutineScope viewModelScope.launch { // 网络请求在 IO 调度器下运行,完全在后台线程执行 val songPage = FunkwhaleRepository.getSongs() // 给内部 MutableLiveData 赋值,postValue 会自动在主线程分发数据 (songListLiveData as MutableLiveData).postValue(songPage.results) } } }
实现要点说明:
- 依托
viewModelScope的生命周期绑定特性,页面销毁时自动取消未完成的网络请求,从根源避免内存泄漏 - 对外仅暴露
LiveData类型的只读引用,保证数据写入入口的唯一性,避免 UI 层随意修改数据源导致的逻辑混乱 - 数据加载逻辑完全封装在 ViewModel 内部,Activity 不需要关注线程切换的细节。
SongListActivity
class SongListActivity : AppCompatActivity() { // 全局持有适配器实例,避免每次数据更新都新建对象 private val songListAdapter = SongListAdapter(emptyList()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.song_list) // 初始化 RecyclerView,布局设置仅执行一次 val rvSongs = findViewById<RecyclerView>(R.id.rvSongs) rvSongs.apply { adapter = songListAdapter layoutManager = LinearLayoutManager(this@SongListActivity) } // 获取 ViewModel 实例 val viewModel = ViewModelProvider(this)[SongListActivityViewModel::class.java] // 观察 LiveData 变化,回调自动运行在主线程 viewModel.songListLiveData.observe(this) { songList -> // 数据更新时刷新适配器数据集,保留原有滚动状态 songListAdapter.updateData(songList) } } }
实现要点说明:
- LiveData 的
observe方法绑定了 Activity 的生命周期,只有页面处于激活状态时才会触发回调,完全不需要手动编写线程切换逻辑 - 适配器仅初始化一次,通过自定义的
updateData方法更新内部数据集,相比每次新建适配器的写法性能更优,也不会丢失 RecyclerView 的滚动状态。
常见疑问澄清
- 针对"LiveData 观察者回调无法在后台线程触发"的问题:这是 LiveData 的主动设计特性,所有观察者回调都会自动切换到主线程执行,后台线程的任务只需要在拉取完数据后调用
postValue把数据提交给 LiveData 即可,完全不需要让观察者本身运行在后台线程 - 针对"拆分请求方法和转 LiveData 方法会过于冗余"的顾虑:正确的职责拆分不需要额外的冗余逻辑,只需要把「后台拉取数据」和「暴露观察数据源」两个逻辑通过 ViewModel 内部的协程串联即可,不存在多余的方法调用开销。
内容的提问来源于stack exchange,提问作者DazedFury
相关产品推荐
相关产品推荐

