要看本系列其他文章,可訪問此連結Jetpack架構學習 | Stars-One的雜貨小窩
原文地址:Jetpack架構元件學習(2)——ViewModel和Livedata使用 | Stars-One的雜貨小窩
Jetpack架構推薦使用MVVM結構,為此推出了幾個MVVM的元件庫供我們開發者快速接入,首先要講的就是ViewModel
個人理解:Activity為View,VM就是ViewModel,負責資料的邏輯處理,Model則是資料來源
ViewModel
介紹
ViewModel能做什麼?
ViewModel生命週期與Activity獨立,可以優雅的儲存記憶體中的資料(在螢幕旋轉的橫豎屏切換時,資料可以得到保留)
可以將ViewMoel看做是資料的處理器和資料倉儲,其只負責處理資料
基本使用
首先,匯入依賴
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中提供了MutableLiveData
和LiveData
兩個類用來包裝資料,這裡先以MutableLiveData
為例講解下使用方式,兩者的不同在下文再補充
我們以上文為例,稍微修改了(主要修改了2
、3
、7
步)
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方法每次返回的都是新的物件,所以每次觀察的物件其實都是新的,而無法觀察到
改造思路:
- 對userId進行資料監聽
- 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
是可變的
LiveData
是MutableLiveData
子類,其裡面的setValue
和postValue
不是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程式碼沒有做任何改變,測試可以發現,效果是一樣的,只是優化了程式碼上的寫法