Jetpack架構組件學習(2)——ViewModel和Livedata使用

要看本系列其他文章,可訪問此鏈接Jetpack架構學習 | Stars-One的雜貨小窩

原文地址:Jetpack架構組件學習(2)——ViewModel和Livedata使用 | Stars-One的雜貨小窩

Jetpack架構推薦使用MVVM結構,為此推出了幾個MVVM的組件庫供我們開發者快速接入,首先要講的就是ViewModel

個人理解:Activity為View,VM就是ViewModel,負責數據的邏輯處理,Model則是數據源

ViewModel

介紹

ViewModel能做什麼?

ViewModel生命周期與Activity獨立,可以優雅的保存記憶體中的數據(在螢幕旋轉的橫豎屏切換時,數據可以得到保留)

可以將ViewMoel看做是數據的處理器和數據倉庫,其只負責處理數據

ViewModel生命周期

基本使用

首先,導入依賴

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

這裡,由於我是使用了Kotlin,所以使用的是具有kotlin特性的版本,如果是純Java,可以使用下述依賴

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-extensions:$lifecycle_version"

下面來個簡單的計數器例子:

class ViewModelActivity : AppCompatActivity() {
    //4.聲明變數
    lateinit var myViewModel:MyViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_model)
        //5.通過ViewModelProvider獲取單一實例對象
        myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        //7.設置按鈕的點擊監聽器
        btnPlus.setOnClickListener {
            myViewModel.countPlus()
            refreshCount()
        }

        refreshCount()
    }

    //6.設置更新數據(暫時,後面會調整為livedata形式)
    fun refreshCount() {
        tvContent.text = myViewModel.count.toString()
    }
}

//1.定義ViewModel
class MyViewModel : ViewModel() {
    //2.定義數據
    var count = 0

    //3.對外暴露方法,用來修改數值
    fun countPlus() {
        count++
    }
}

效果如下所示:

為什麼上面需要使用ViewmodelProvider來獲取單一對象?原因是ViewModel的生命周期是獨立於Activity,可以臨時保存數據

上述還是使用的比較傳統的方式,在對應按鈕的點擊監聽對UI進行修改,之後會使用LiveData進行改造

ViewModel的構造函數傳參

由於之前說過ViewModel是單例模式,所以想要傳參,需要藉助ViewModelProvider.Factory這個介面類來實現

假如上面我MainViewModel需要接收一個Activity中傳來的參數,我們可以這樣寫:

將原來的MainViewModel類增加個構造方法

class MyViewModel(val saveCount: Int?) : ViewModel() {
    var count = 0

    init {
        //不傳參數的話,則默認是0
        count = saveCount ?: 0
    }

    fun countPlus() {
        count++
    }
}

之後,定義個工廠類MyViewModelFactory去實現ViewModelProvider.Factory介面

class MyViewModelFactory(val myCount: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        //這裡使用構造方法傳參
        return MyViewModel(myCount) as T
    }
}

PS:可以把MyViewModelFactory寫在MyViewModel中,這樣沒必要再整多一個文件了

在Activity中,我們需要新建一個MyViewModelFactory對象和ViewModelProvider一起使用即可,程式碼如下所示

myViewModel = ViewModelProvider(this,MyViewModelFactory(12)).get(MyViewModel::class.java)

可以看到,現在默認是從12開始了,如下圖所示

PS:感覺想要實現Activity中給ViewModel傳參的話,步驟是有點多的,不過考慮下架構,這種使用構造方法進行傳參其實不太符合MVVM架構。

因為Activity改變數據,觸發對應的數據更改即可,而不是在構造方法的時候傳參;不過,也可以會有特殊需求(比如說ViewModel中需要context對象),所以才留下了這個實現方式吧

AndroidViewModel(ViewModel擴展類)

上文說到,如果ViewModel中需要Context對象,我們怎麼辦呢?

經常遇到的情況,是需要獲取一個Context上下文對象,可能你想到,我們把當前的Activity傳入到ViewModel中不就可以了嗎?

這樣做雖然是可以,不過會引起其他的問題,會導致ViewModel與Activity耦合過深,原本設計ViewModel就是為了減少耦合,這樣做卻是本末倒置了

使用ViewModel的時候,需要注意的是ViewModel不能夠持有View、Lifecycle、Acitivity引用,而且不能夠包含任何包含前面內容的類。因為這樣很有可能會造成記憶體泄漏。

開發團隊也是考慮到這樣的問題,提供了一個子類AndroidViewModel供我們使用,其也是繼承於ViewModel,其中存在有個application實例(即application對象)

可以看下AndroidViewModel的源碼

由於每個APP只有一個application對象,所以就不用擔心會出現上述問題

使用的話,和上述是一樣,也需要使用到Factory介面,不過無需我們去實現了的,我們使用內置的ViewModelProvider.AndroidViewModelFactory這個類即可

具體程式碼如下所示:

class MyViewModel(application: Application) : AndroidViewModel(application) {

    fun getCacheDirPath() :String {
        //MyApplication是我的自定義Application入口類,如果沒有使用自定義的Application,這裡直接寫Application即可
        var application = getApplication<MyApplication>()
        //使用application對象獲取快取目錄路徑
        return application.cacheDir.path
    }
}

Activity中使用:

 myViewModel = ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(MyViewModel::class.java)

可以看到TextView設置的數值即為路徑

進階用法

  • 由於之前提及到ViewModel實際是單例模式的,且生命周期與Activity獨立,所以可以使用ViewModel進行Activity和Fragment之間或Fragment之間的數據共享

LiveData

上述ViewModel只是提供了個數據倉庫,如果我們使用傳統的對象是無法實現MVVM架構的,這個時候就得使用LiveData

LiveData即相當於給數據加多一層包裝,讓數據可以被觀察

由於LiveData內部也是使用的LifeCycle實現的,所以它設計成當數據發生改變時候,只要在頁面可見狀態才會觸發頁面改變,節省資源及錯誤的發生

基本使用

1.導入依賴

首先還是導入依賴,版本與上述使用的版本一致即可,根據項目所屬類型選擇對應的版本

//kotlin特性版本
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

//Java版本
implementation "androidx.lifecycle:lifecycle-livedata-extensions:$lifecycle_version"

2.使用MutableLiveData來包裝數據

LiveData中提供了MutableLiveDataLiveData兩個類用來包裝數據,這裡先以MutableLiveData為例講解下使用方式,兩者的不同在下文再補充

我們以上文為例,稍微修改了(主要修改了237步)

class ViewModelActivity : AppCompatActivity() {
    //4.聲明變數
    lateinit var myViewModel:MyViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_model)
        //5.通過ViewModelProvider獲取單一實例對象
        myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        //監聽數據,UI更改
        myViewModel.count.observe(this){
            tvContent.text = it.toString()
        }

        //7.設置按鈕的點擊監聽器
        btnPlus.setOnClickListener {
            //點擊操作只處罰對應的數據修改,不做更新UI操作
            myViewModel.countPlus()
        }

    }

}

//1.定義ViewModel
class MyViewModel : ViewModel() {
    //2.定義數據
    var count = MutableLiveData<Int>(0)

    //3.對外暴露方法,用來修改數值
    fun countPlus() {
        val value = count.value as Int
        count.value = value+1
    }
}

這裡的主要區別就是,我們不在點擊事件中去更改UI,更改UI的操作則是寫了個數據監聽方法,去監聽數據更改再更新UI

雖然這裡感覺還是和之前一樣,也是要通過TextView對象去修改數值,但是,從程式碼上來看,數據處理邏已經和頁面渲染分離開來了,也是方便我們的編寫測試用例

setValue()postValue()的區別

上述程式碼中,我們通過count.value=value+1來設置數據,這裡由於是kotlin的寫法所以看不出來是setValue()方法,如果是Java的話,是要調用setValue()方法來設置數據

除了setValue(),LiveData還提供了postValue()方法,這兩種的方法區別在於setValue()要再主執行緒(UI執行緒)才能操作,而postValue()則是在子執行緒或主執行緒都可以

我們把上述程式碼中的第7步稍微改動下,點擊按鈕就開啟一個執行緒,然後等待1s後才更新數據

然後運行的時候報錯了,如下圖所示

把第3步的設置數據操作改下

就可以正常運行了,點擊按鈕會等1s後,數據才會發生變化,如下圖:

map

此方法主要是將LiveData包裝的數據類轉為單一另外類型的LiveData,舉個例子說明

比如我們有個User類,裡面包含兩個欄位(姓名和年齡),而我們頁面只需要觀察姓名的改變,而不關心年齡的改變,那麼我們就可以沒有必要把整個對象都觀察

只需要像下面這樣寫,可以把MutableLiveData<User>轉為MutableLiveData<String>

注:以下程式碼是在ViewMode抽出來的一段程式碼

data class User(var name: String, var age: Int)
//改為private,不對外提供訪問
private val user = MutableLiveData<User>(User("張三", 5))

val userName = Transformations.map(user){ it.name }

之後,我們在Activity只需要對userName數據進行監聽,改變UI即可

PS:補充一下,LiveData中是無法做到對對象中的某個欄位進行監聽,只能做到對對象記憶體地址進行監聽

如果兩個對象的記憶體地址是相同的,那麼不會觸發對應的數據改變監聽事件

若是想要實現,目前個人摸索的方式就是使用koltin擴展方法copy(),如下程式碼,就是快速複製一個對象,且改變其的某個欄位的數據,之後即可正常觸發數據變更的監聽事件

val user = User("zz", 11)
val newUser = user.copy(name = "hello")

switchMap

前面所講述內容,LiveData的對象都是都是位於同個ViewModel中,但實際情況,我們需要從別的地方拿取數據,這個時候就是可以考慮使用此方法

假設我們User對象是要通過userId來獲取,定義一個單例,實現上述功能

object UserRepository{
    fun getUserById(userId: String) :LiveData<User>{
        val userLiveData = MutableLiveData<User>()
        userLiveData.value = User("張三$userId",15)
        return userLiveData
    }
}

在ViewModel中定個方法去實現獲取數據

class MyViewModel : ViewModel() {

    fun getUser(userId: String): LiveData<User> {
        return UserRepository.getUserById(userId)
    }
}

這個時候,我們想要觀察這個對象改變從而渲染UI,應該如何做呢?

估計大部分人都會想到下面的程式碼

myViewModel.getUser("111").observe(this){
    //todo UI渲染
}

但是注意,之前UserRepository中的getUser方法每次返回的都是新的對象,所以每次觀察的對象其實都是新的,而無法觀察到

改造思路:

  1. 對userId進行數據監聽
  2. userId變更,同時觸發user對象的變更
class MyViewModel : ViewModel() {

    val userIdLiveData = MutableLiveData("")
    
    //user是MutableLiveData<User>對象
    val user = Transformations.switchMap(userIdLiveData){
        UserRepository.getUserById(it)
    }
    
    fun getUser(userId: String){
        userIdLiveData.value = userId
    }
}

Activity中的程式碼:

myViewModel.user.observe(this){
    tvContent.text = it.name
}

//7.設置按鈕的點擊監聽器
btnPlus.setOnClickListener {
    //點擊操作只處罰對應的數據修改,不做更新UI操作
    myViewModel.getUser("445")
}

效果如下:

PS:如果是不傳參數的,可以設置一個MutableLiveData<Any?>對象,並讓其重新賦值即可實現更新數據,如下程式碼

object UserRepository{

    fun refresh() :LiveData<User>{
        val userLiveData = MutableLiveData<User>()
        userLiveData.value = User("張三", 15)
        return userLiveData
    }
}

class MyViewModel : ViewModel() {

    val refreshLiveData = MutableLiveData<Any?>()
    
    fun refresh() {
        //會觸發對應的數據變更通知
        refreshLiveData.value = refreshLiveData.value
    }

    val user = Transformations.switchMap(refreshLiveData){
        UserRepository.refresh() 
    }
    
}

LiveData和MutableLiveData兩者區別

LiveData是不可變的,MutableLiveData是可變的

LiveDataMutableLiveData子類,其裡面的setValuepostValue不是public,因此無法在外部調用,其只能註冊觀察者

MutableLiveData重寫了方法(如下圖),並聲明為public,所以我們才能在任意地方可以調用進行數值的修改(不過推薦還是在ViewModel中進行數據更改的操作)

程式碼優化

上述雖然實現了基本的使用,但是ViewModel中的封裝的程式碼還存在不安全性,原因是我們的count變數,只要某個地方拿到了這個ViewModel的對象,就直接拿到這個count變數,從而進行修改

這樣就會造成數據問題,所以我們得實現可信任源才能對數據進行修改

官方推薦的做法:

1.ViewModel對外提供不可變的可觀察數據LiveData對象
2.由外部調用方法才能改變內部數據

class MyViewModel : ViewModel() {
    //_count是只能在ViewModel內部修改
    private val _count =MutableLiveData(0)

    //對外提供的變數,Activity中可註冊觀察者從而修改UI
    val count :LiveData<Int> get() = _count

    //修改數據的方法
    fun countPlus() {
        val value = count.value as Int
        _count.value = value+1
    }
}

Activity程式碼沒有做任何改變,測試可以發現,效果是一樣的,只是優化了程式碼上的寫法

參考