JetPack知识点实战系列五:歌单页面MVVM架构改造及其ViewModel和LiveData的使用介绍

JetPack有提供规范的架构模式,我们使用JetPack,必须要遵循它的规范,接下来我们将利用JetPack实现MVVM的架构模式。

MVC和MVVM介绍

MVC

我们目前的代码主要逻辑和数据都在Activity/Fragment中,有人定义为MVC架构,有人却不这么认为。因为Activity/FragmentView又是很难完全区分开来,和Java后台开发中完全的MVC模式有差别。我们暂且把这中模式定义为MVC模式吧。

咱们画个简单的示意图:

MVVM模式

通过示意图我们可以看出,Activity/Fragment作为ControllerView的组合体,分担的任务比较繁重,这里面的代码会非常的臃肿。

为了解决这个问题,Google通过JetPack的架构规范了MVVM的架构模式。

MVVM

我们先通过Google官网的一张图片来了解下他们规范的架构模式:

Google架构图

这张图片定义了Activity/Fragment如何获取数据的分层模式。

  1. Activity/Fragment持有ViewModelViewModel是专门负责数据管理的类
  2. ViewModel管理LiveData中的数据
  3. LiveData的数据是从仓库Repository获得
  4. Repository又是从数据库Room或者网络webService获得

细心的你可能发现了,这个分层非常详细,但是只是数据的单向获取流程,获取到的数据如何和UI的重绘联系起来没有体现出来。

接下来我用一个详尽的示意图解释下:

MVVM

这个示例图增加了数据回流的过程,数据从数据库或者网络服务器中获取后,通过CallBack或者LiveData反向回流到ViewModel数据管理类。

注意了,这时候ViewModel中的LiveData数据直接驱动了界面的重绘,无需经过Activity/Fragment的转发。

此外,Jetpack还提供了数据绑定,使用数据绑定DataBindingActivity/Fragment不需要持有View引用,当然也不会有View的事件驱动,流程如下所示:

MVVM+DataBinding

到此为止,我们已经了解到了JetPack的架构结构,是时候应用到我们的项目中来啦。

修改歌单页面

接着上个教程的结尾,我们给歌单列表页面添加一个PlayListFragmentViewModelViewModel对象,来作为这个页面的数据管理类。

  • 加入依赖库
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
                }
            }
        }
    }

}

我们来分步骤解释下代码的含义:

  1. 定义一个值为List<PlayItem>MutableLiveData变量_playList,这里MutableLiveData就是值可以改变的LiveData
  2. 定义一个值为List<PlayItem>LiveData变量playList, 定义这个变量的意义是把_playList封装起来,只能内部修改它的值,提供给外部是的不能修改的值
  3. 这个变量是传入的不同的歌单类型
  4. fetchData这个方法是请求数据的方法
  5. viewModelScope这个是和ViewModel关联的协程作用域,这个作用域的生命周期和ViewModel一致,超过这个作用域协程会被取消。

协程和协程作用域的相关知识请参考前面的教程

  1. 通过PlaylistRepository对象去请求数据,这个类后面介绍
  2. 将请求得到的结果赋值给_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函数才能调用MusicApiServicesuspend函数

  • 改造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()

    }

}

代码解释如下:

  1. 通过by委托模式生成PlayListViewModel对象,PlayListFragment只留下这个viewModel属性。
  2. 将歌单类型赋值给ViewModeltype
  3. 这个viewModel.playListLiveData,这句代码就含义是LiveData调用observe方法。那这段话代表什么呢?

官网解释:LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。

用过RxJava的同学对LiveData 是一种可观察的数据存储器类这句话有比较好的理解,就是说LiveData对象数据的变化可以及时通知观察者,观察者可以通过根据数据进行相关的操作。

这里的observe方法就是添加观察者,第一个参数决定了观察的生命周期,第二个lambda参数就是观测到数据变化后的操作

  1. 将数据提交个Adapter

细心的读者可能会发现不是使用adapter.notifyDataSetChanged,这是因为这个方法性能更好,可以通过DiffUtil.ItemCallback对比数据的差异,只对更新的数据进行动画和刷新,而不是一股脑的所有界面都刷新。

  1. 调用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
        }
    }

}
  1. DiffUtil.ItemCallback是在PlaylistItemAdapter构造函数中传入的。具体代码如下:
  2. DiffUtil.ItemCallback需要实现两个方法,areItemsTheSame是判断两个Item是否是同一个Item,areContentsTheSame是判断两个Item是否内容相同。
遗留问题2 - 为什么先判断LiveDatavalue存不存在?

我们先来看一个现象:

常规写法:

正常写法

切换横竖屏,Fragment会重新创建,所以切换完成后会重新请求数据。

用ViewModel的写法:

ViewModel

切换横竖屏,Fragment会重新创建,切换完成后并没有请求数据,但是还能正常显示列表。

为什么呢?先看一张官网的图和对ViewModel生命周期的解释

生命周期图

ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。

相信看到这里,你就理解了为什么onViewCreated的时候LiveDatavalue有可能存在了。

结语

目前我们已经将歌单页面改造成了MVVM的架构了,接下来我们将继续上一节的内容,利用PageingLiveData实现加载更多。

猜你喜欢

转载自blog.csdn.net/lcl130/article/details/108567097