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实现加载更多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270