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

one發表於2021-11-25

要看本系列其他文章,可訪問此連結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程式碼沒有做任何改變,測試可以發現,效果是一樣的,只是優化了程式碼上的寫法

參考

相關文章