JetPack有提供规范的架构模式,我们使用JetPack,必须要遵循它的规范,接下来我们将利用JetPack实现MVVM的架构模式。
MVC和MVVM介绍
MVC
我们目前的代码主要逻辑和数据都在Activity/Fragment中,有人定义为MVC架构,有人却不这么认为。因为Activity/Fragment和View又是很难完全区分开来,和Java后台开发中完全的MVC模式有差别。我们暂且把这中模式定义为MVC模式吧。
咱们画个简单的示意图:
通过示意图我们可以看出,Activity/Fragment作为Controller和View的组合体,分担的任务比较繁重,这里面的代码会非常的臃肿。
为了解决这个问题,Google通过JetPack的架构规范了MVVM的架构模式。
MVVM
我们先通过Google官网的一张图片来了解下他们规范的架构模式:
这张图片定义了Activity/Fragment如何获取数据的分层模式。
- Activity/Fragment持有ViewModel,ViewModel是专门负责数据管理的类
- ViewModel管理LiveData中的数据
- LiveData的数据是从仓库Repository获得
- Repository又是从数据库Room或者网络webService获得
细心的你可能发现了,这个分层非常详细,但是只是数据的单向获取流程,获取到的数据如何和UI的重绘联系起来没有体现出来。
接下来我用一个详尽的示意图解释下:
这个示例图增加了数据回流的过程,数据从数据库或者网络服务器中获取后,通过CallBack或者LiveData反向回流到ViewModel数据管理类。
注意了,这时候ViewModel中的LiveData数据直接驱动了界面的重绘,无需经过Activity/Fragment的转发。
此外,Jetpack还提供了数据绑定,使用数据绑定DataBinding后Activity/Fragment不需要持有View引用,当然也不会有View的事件驱动,流程如下所示:
到此为止,我们已经了解到了JetPack的架构结构,是时候应用到我们的项目中来啦。
修改歌单页面
接着上个教程的结尾,我们给歌单列表页面添加一个PlayListFragmentViewModel的ViewModel对象,来作为这个页面的数据管理类。
- 加入依赖库
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
通过引入的库我们可以看到ViewModel存在于lifecycle库,也就是说它能感知生命周期的变化。
- 新建PlayListViewModel文件
我们新建一个ViewModel的子类PlayListViewModel,作为歌单列表的ViewModel类,类中的代码如下所示:
class PlayListViewModel: ViewModel() {
// 1
private val _playList = MutableLiveData<List<PlayItem>>()
// 2
val playList: LiveData<List<PlayItem>>
get() = _playList
// 3
var type: String = ""
// 4
fun fetchData() {
// 5
viewModelScope.launch {
when (type) {
"推荐" -> {
// 6
val response = PlaylistRepository.getRecommendPlaylist(30, 0)
// 7
_playList.value = response.playlists
}
"精品" -> {
val response = PlaylistRepository.getHighQualityPlaylist(30, 0)
_playList.value = response.playlists
}
"官方" -> {
val response = PlaylistRepository.getOrgPlaylist(30, 0)
_playList.value = response.playlists
}
else -> {
val response = PlaylistRepository.getPlaylistByCat(30, 0, type)
_playList.value = response.playlists
}
}
}
}
}
我们来分步骤解释下代码的含义:
- 定义一个值为
List<PlayItem>
的MutableLiveData变量_playList
,这里MutableLiveData就是值可以改变的LiveData - 定义一个值为
List<PlayItem>
的LiveData变量playList
, 定义这个变量的意义是把_playList
封装起来,只能内部修改它的值,提供给外部是的不能修改的值 - 这个变量是传入的不同的歌单类型
-
fetchData
这个方法是请求数据的方法 - viewModelScope这个是和ViewModel关联的协程作用域,这个作用域的生命周期和ViewModel一致,超过这个作用域协程会被取消。
协程和协程作用域的相关知识请参考前面的教程
- 通过PlaylistRepository对象去请求数据,这个类后面介绍
- 将请求得到的结果赋值给
_playList
- PlaylistRepository文件
实际上可以直接用MusicApiService进行请求,为什么会多一个Repository层。其实是为了模块化的方便,因为一个大型项目会有很多的功能块,请求也会非常的的多,这样建立多个Repository*进行模块分组,是个非常不错的实践。
PlaylistRepository的代码如下所示,
object PlaylistRepository {
/* 获取推荐歌单列表 */
suspend fun getRecommendPlaylist(limit: Int, offset: Int) : PlayListResponse {
return MusicApiService.create().getRecommendPlaylist(limit, offset)
}
/* 获取金品歌单列表 */
suspend fun getHighQualityPlaylist(limit: Int, offset: Int) : PlayListResponse {
return MusicApiService.create().getHighQualityPlaylist(limit, offset)
}
/* 获取官方歌单列表 */
suspend fun getOrgPlaylist(limit: Int, offset: Int): PlayListResponse {
return MusicApiService.create().getCatPlaylist(limit, offset, "new", null)
}
/* 根据类别获取歌单列表 */
suspend fun getPlaylistByCat(limit: Int, offset: Int, cat: String): PlayListResponse {
return MusicApiService.create().getCatPlaylist(limit, offset, null, cat)
}
}
这段代码很好理解,但是需要说明一点,suspend函数必须在suspend函数中或者协程中调用,所以这个文件的方法也都是设计成suspend函数才能调用MusicApiService的suspend函数。
- 改造PlayListFragment使用ViewModel
class PlayListFragment : Fragment() {
// 1
private val viewModel by viewModels<PlayListViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// 2
arguments?.getString(QueryKey)?.let {
viewModel.type = it
}
// 3
viewModel.playList.observe(viewLifecycleOwner, Observer {
// 4
playAdapter.submitList(it)
})
// 5
viewModel.playList.value ?: viewModel.fetchData()
}
}
代码解释如下:
- 通过by委托模式生成PlayListViewModel对象,PlayListFragment只留下这个viewModel属性。
- 将歌单类型赋值给ViewModel的type
- 这个viewModel.playList是LiveData,这句代码就含义是LiveData调用
observe
方法。那这段话代表什么呢?
官网解释:LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。
用过RxJava的同学对LiveData 是一种可观察的数据存储器类这句话有比较好的理解,就是说LiveData对象数据的变化可以及时通知观察者,观察者可以通过根据数据进行相关的操作。
这里的
observe
方法就是添加观察者,第一个参数决定了观察的生命周期,第二个lambda参数就是观测到数据变化后的操作
- 将数据提交个Adapter
细心的读者可能会发现不是使用adapter.notifyDataSetChanged,这是因为这个方法性能更好,可以通过DiffUtil.ItemCallback对比数据的差异,只对更新的数据进行动画和刷新,而不是一股脑的所有界面都刷新。
- 调用
viewModel.fetchData()
方法
这里遗留了一个问题,为什么先判断value存不存在,存在就不请求了呢?不是应该
onViewCreated
这时候value肯定是不存在的吗?
遗留问题1 - DiffUtil.ItemCallback怎么使用
// 1
class PlaylistItemAdapter:
ListAdapter<PlayItem, PlaylistItemAdapter.PlaylistItemHolder>(DiffCallback) {
// 2
object DiffCallback: DiffUtil.ItemCallback<PlayItem>() {
override fun areItemsTheSame(oldItem: PlayItem, newItem: PlayItem): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: PlayItem, newItem: PlayItem): Boolean {
return oldItem.name == newItem.name && oldItem.coverImgUrl == newItem.coverImgUrl
}
}
}
- DiffUtil.ItemCallback是在PlaylistItemAdapter构造函数中传入的。
- DiffUtil.ItemCallback需要实现两个方法,areItemsTheSame是判断两个Item是否是同一个Item,areContentsTheSame是判断两个Item是否内容相同。
遗留问题2 - 为什么先判断LiveData的value存不存在?
我们先来看一个现象:
常规写法:
切换横竖屏,Fragment会重新创建,所以切换完成后会重新请求数据。
用ViewModel的写法:
切换横竖屏,Fragment会重新创建,切换完成后并没有请求数据,但是还能正常显示列表。
为什么呢?先看一张官网的图和对ViewModel生命周期的解释
ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。
相信看到这里,你就理解了为什么onViewCreated的时候LiveData的value有可能存在了。
结语
目前我们已经将歌单页面改造成了MVVM的架构了,接下来我们将继续上一节的内容,利用Pageing和LiveData实现加载更多。