JetPack知識點(diǎn)實(shí)戰(zhàn)系列五:歌單頁面MVVM架構(gòu)改造及其ViewModel和LiveData的使用介紹

JetPack有提供規(guī)范的架構(gòu)模式旦委,我們使用JetPack,必須要遵循它的規(guī)范雏亚,接下來我們將利用JetPack實(shí)現(xiàn)MVVM的架構(gòu)模式缨硝。

MVC和MVVM介紹

MVC

我們目前的代碼主要邏輯和數(shù)據(jù)都在Activity/Fragment中,有人定義為MVC架構(gòu)评凝,有人卻不這么認(rèn)為追葡。因?yàn)?strong>Activity/Fragment和View又是很難完全區(qū)分開來,和Java后臺(tái)開發(fā)中完全的MVC模式有差別奕短。我們暫且把這中模式定義為MVC模式吧宜肉。

咱們畫個(gè)簡單的示意圖:

MVVM模式

通過示意圖我們可以看出,Activity/Fragment作為ControllerView的組合體翎碑,分擔(dān)的任務(wù)比較繁重谬返,這里面的代碼會(huì)非常的臃腫。

為了解決這個(gè)問題日杈,Google通過JetPack的架構(gòu)規(guī)范了MVVM的架構(gòu)模式遣铝。

MVVM

我們先通過Google官網(wǎng)的一張圖片來了解下他們規(guī)范的架構(gòu)模式:

Google架構(gòu)圖

這張圖片定義了Activity/Fragment如何獲取數(shù)據(jù)的分層模式佑刷。

  1. Activity/Fragment持有ViewModelViewModel是專門負(fù)責(zé)數(shù)據(jù)管理的類
  2. ViewModel管理LiveData中的數(shù)據(jù)
  3. LiveData的數(shù)據(jù)是從倉庫Repository獲得
  4. Repository又是從數(shù)據(jù)庫Room或者網(wǎng)絡(luò)webService獲得

細(xì)心的你可能發(fā)現(xiàn)了酿炸,這個(gè)分層非常詳細(xì)瘫絮,但是只是數(shù)據(jù)的單向獲取流程,獲取到的數(shù)據(jù)如何和UI的重繪聯(lián)系起來沒有體現(xiàn)出來填硕。

接下來我用一個(gè)詳盡的示意圖解釋下:

MVVM

這個(gè)示例圖增加了數(shù)據(jù)回流的過程麦萤,數(shù)據(jù)從數(shù)據(jù)庫或者網(wǎng)絡(luò)服務(wù)器中獲取后,通過CallBack或者LiveData反向回流到ViewModel數(shù)據(jù)管理類扁眯。

注意了壮莹,這時(shí)候ViewModel中的LiveData數(shù)據(jù)直接驅(qū)動(dòng)了界面的重繪,無需經(jīng)過Activity/Fragment的轉(zhuǎn)發(fā)姻檀。

此外命满,Jetpack還提供了數(shù)據(jù)綁定,使用數(shù)據(jù)綁定DataBindingActivity/Fragment不需要持有View引用绣版,當(dāng)然也不會(huì)有View的事件驅(qū)動(dòng)胶台,流程如下所示:

MVVM+DataBinding

到此為止,我們已經(jīng)了解到了JetPack的架構(gòu)結(jié)構(gòu)僵娃,是時(shí)候應(yīng)用到我們的項(xiàng)目中來啦概作。

修改歌單頁面

接著上個(gè)教程的結(jié)尾,我們給歌單列表頁面添加一個(gè)PlayListFragmentViewModelViewModel對象默怨,來作為這個(gè)頁面的數(shù)據(jù)管理類讯榕。

  • 加入依賴庫
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

通過引入的庫我們可以看到ViewModel存在于lifecycle庫,也就是說它能感知生命周期的變化匙睹。

  • 新建PlayListViewModel文件

我們新建一個(gè)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. 定義一個(gè)值為List<PlayItem>MutableLiveData變量_playList,這里MutableLiveData就是值可以改變的LiveData
  2. 定義一個(gè)值為List<PlayItem>LiveData變量playList, 定義這個(gè)變量的意義是把_playList封裝起來痕檬,只能內(nèi)部修改它的值霎槐,提供給外部是的不能修改的值
  3. 這個(gè)變量是傳入的不同的歌單類型
  4. fetchData這個(gè)方法是請求數(shù)據(jù)的方法
  5. viewModelScope這個(gè)是和ViewModel關(guān)聯(lián)的協(xié)程作用域,這個(gè)作用域的生命周期和ViewModel一致梦谜,超過這個(gè)作用域協(xié)程會(huì)被取消丘跌。

協(xié)程和協(xié)程作用域的相關(guān)知識請參考前面的教程

  1. 通過PlaylistRepository對象去請求數(shù)據(jù),這個(gè)類后面介紹
  2. 將請求得到的結(jié)果賦值給_playList
  • PlaylistRepository文件

實(shí)際上可以直接用MusicApiService進(jìn)行請求唁桩,為什么會(huì)多一個(gè)Repository層闭树。其實(shí)是為了模塊化的方便,因?yàn)橐粋€(gè)大型項(xiàng)目會(huì)有很多的功能塊荒澡,請求也會(huì)非常的的多报辱,這樣建立多個(gè)Repository*進(jìn)行模塊分組,是個(gè)非常不錯(cuò)的實(shí)踐单山。

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)
    }

    /* 根據(jù)類別獲取歌單列表 */
    suspend fun getPlaylistByCat(limit: Int, offset: Int, cat: String): PlayListResponse {
        return MusicApiService.create().getCatPlaylist(limit, offset, null, cat)
    }

}

這段代碼很好理解幅疼,但是需要說明一點(diǎn),suspend函數(shù)必須在suspend函數(shù)中或者協(xié)程中調(diào)用昼接,所以這個(gè)文件的方法也都是設(shè)計(jì)成suspend函數(shù)才能調(diào)用MusicApiServicesuspend函數(shù)爽篷。

  • 改造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只留下這個(gè)viewModel屬性慢睡。
  2. 將歌單類型賦值給ViewModeltype
  3. 這個(gè)viewModel.playListLiveData狼忱,這句代碼就含義是LiveData調(diào)用observe方法。那這段話代表什么呢一睁?

官網(wǎng)解釋:LiveData 是一種可觀察的數(shù)據(jù)存儲(chǔ)器類。與常規(guī)的可觀察類不同佃却,LiveData 具有生命周期感知能力者吁,意指它遵循其他應(yīng)用組件(如 Activity、Fragment 或 Service)的生命周期饲帅。

用過RxJava的同學(xué)對LiveData 是一種可觀察的數(shù)據(jù)存儲(chǔ)器類這句話有比較好的理解复凳,就是說LiveData對象數(shù)據(jù)的變化可以及時(shí)通知觀察者,觀察者可以通過根據(jù)數(shù)據(jù)進(jìn)行相關(guān)的操作灶泵。

這里的observe方法就是添加觀察者育八,第一個(gè)參數(shù)決定了觀察的生命周期,第二個(gè)lambda參數(shù)就是觀測到數(shù)據(jù)變化后的操作

  1. 將數(shù)據(jù)提交個(gè)Adapter

細(xì)心的讀者可能會(huì)發(fā)現(xiàn)不是使用adapter.notifyDataSetChanged赦邻,這是因?yàn)檫@個(gè)方法性能更好髓棋,可以通過DiffUtil.ItemCallback對比數(shù)據(jù)的差異,只對更新的數(shù)據(jù)進(jìn)行動(dòng)畫和刷新惶洲,而不是一股腦的所有界面都刷新按声。

  1. 調(diào)用viewModel.fetchData()方法

這里遺留了一個(gè)問題,為什么先判斷value存不存在恬吕,存在就不請求了呢签则?不是應(yīng)該onViewCreated這時(shí)候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構(gòu)造函數(shù)中傳入的铐料。
  2. DiffUtil.ItemCallback需要實(shí)現(xiàn)兩個(gè)方法渐裂,areItemsTheSame是判斷兩個(gè)Item是否是同一個(gè)Item,areContentsTheSame是判斷兩個(gè)Item是否內(nèi)容相同钠惩。
遺留問題2 - 為什么先判斷LiveDatavalue存不存在柒凉?

我們先來看一個(gè)現(xiàn)象:

常規(guī)寫法:

正常寫法

切換橫豎屏,F(xiàn)ragment會(huì)重新創(chuàng)建妻柒,所以切換完成后會(huì)重新請求數(shù)據(jù)扛拨。

用ViewModel的寫法:

ViewModel

切換橫豎屏,F(xiàn)ragment會(huì)重新創(chuàng)建举塔,切換完成后并沒有請求數(shù)據(jù)绑警,但是還能正常顯示列表求泰。

為什么呢?先看一張官網(wǎng)的圖和對ViewModel生命周期的解釋

生命周期圖

ViewModel 對象存在的時(shí)間范圍是獲取 ViewModel 時(shí)傳遞給 ViewModelProvider 的 Lifecycle计盒。ViewModel 將一直留在內(nèi)存中渴频,直到限定其存在時(shí)間范圍的 Lifecycle 永久消失:對于 Activity,是在 Activity 完成時(shí)北启;而對于 Fragment卜朗,是在 Fragment 分離時(shí)。

相信看到這里咕村,你就理解了為什么onViewCreated的時(shí)候LiveDatavalue有可能存在了场钉。

結(jié)語

目前我們已經(jīng)將歌單頁面改造成了MVVM的架構(gòu)了,接下來我們將繼續(xù)上一節(jié)的內(nèi)容懈涛,利用PageingLiveData實(shí)現(xiàn)加載更多逛万。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市批钠,隨后出現(xiàn)的幾起案子宇植,更是在濱河造成了極大的恐慌,老刑警劉巖埋心,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件指郁,死亡現(xiàn)場離奇詭異,居然都是意外死亡拷呆,警方通過查閱死者的電腦和手機(jī)闲坎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洋腮,“玉大人箫柳,你說我怎么就攤上這事∩豆” “怎么了悯恍?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伙狐。 經(jīng)常有香客問我涮毫,道長,這世上最難降的妖魔是什么贷屎? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任罢防,我火速辦了婚禮,結(jié)果婚禮上唉侄,老公的妹妹穿的比我還像新娘咒吐。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布恬叹。 她就那樣靜靜地躺著候生,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绽昼。 梳的紋絲不亂的頭發(fā)上唯鸭,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天,我揣著相機(jī)與錄音硅确,去河邊找鬼目溉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛菱农,可吹牛的內(nèi)容都是我干的缭付。 我是一名探鬼主播,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼循未,長吁一口氣:“原來是場噩夢啊……” “哼蛉腌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起只厘,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舅巷,沒想到半個(gè)月后羔味,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钠右,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年赋元,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片飒房。...
    茶點(diǎn)故事閱讀 38,683評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搁凸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出狠毯,到底是詐尸還是另有隱情护糖,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布嚼松,位于F島的核電站嫡良,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏献酗。R本人自食惡果不足惜寝受,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罕偎。 院中可真熱鬧很澄,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浪藻,卻和暖如春捐迫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爱葵。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工施戴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人萌丈。 一個(gè)月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓赞哗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辆雾。 傳聞我的和親對象是個(gè)殘疾皇子肪笋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,566評論 2 349