AndroidJetpack Livedata應用場景分析
## Livedata 概覽
LiveData 是一種可觀察的資料儲存器類。與常規的可觀察類不同,LiveData 具有生命週期感知能力
如果觀察者(由 Observer 類表示)的生命週期處於 STARTED 或 RESUMED 狀態,則 LiveData 會認為該觀察者處於活躍狀態。。LiveData 只會將更新通知給活躍的觀察者。為觀察 LiveData 物件而註冊的非活躍觀察者不會收到更改通知。
您可以註冊與實現 LifecycleOwner 介面的物件配對的觀察者。有了這種關係,當相應的 Lifecycle 物件的狀態變為 DESTROYED 時,便可移除此觀察者。這對於 Activity 和 Fragment 特別有用,因為它們可以放心地觀察 LiveData 物件,而不必擔心洩露
## LiveData 優勢
1. 資料符合頁面狀態
2. 不會發生記憶體洩露
3. 不會因 activity 停止而導致崩潰
4. 不再需要手動處理生命週期
5. 資料始終保持最新狀態
6. 可以用來做資源共享
## Livedata 使用
一般來說我們會在 ViewModel 中建立 Livedata 物件,然後再 Activity/Fragment 的 onCreate 中註冊 Livedata 監聽(因為在 onStart 和 onResume 中進行監聽可能會有冗餘呼叫)
### Livedata 簡單使用
仍然還是用我們倒計時的例子,在 Viewmodel 中開始一個 2000s 的倒計時,然後通過 Livedata 回撥給 Activity 進行更新介面,程式碼:
1. viewmodel 程式碼
```
class CountDownModel : ViewModel() {
val countDownLivedata = MutableLiveData<String>()
private var remainSecond = 2000//剩餘秒數
init {
val countDown = object : CountDownTimer(2000 * 1000, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainSecond--
countDownLivedata.postValue("剩餘:${remainSecond} 秒")
}
override fun onFinish() {
countDownLivedata.postValue("倒計時結束")
}
}
countDown.start()
}
}
複製程式碼
```
2. activity 中觀察資料更新 ui 程式碼
```
val countDownModel: CountDownModel by viewModels<CountDownModel> {
ViewModelProvider.NewInstanceFactory()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_count_down)
countDownModel.countDownLivedata.observe(this, object : Observer<String> {
override fun onChanged(value: String?) {
value?.let {
tv_countdown_remainsecond.text = it
}
}
})
}
複製程式碼
```
3. 效果圖
![image](//upload-images.jianshu.io/upload_images/23587538-417835c7b9a34922.image?imageMogr2/auto-orient/strip|imageView2/2/w/217/format/webp)
### 使用全域性 Livedata 在多個檢視監聽狀態
本例實現的 demo 效果是,建立一個全域性的倒計時,然後在 Activity 中新增兩個按鈕,點選後可以切換 FragmentA 和 FragmentB。然後我們通過全域性的自定義 LiveData 單例實現資料監聽,切換 Fragment 後 Fragment 頁面上會展示倒計時的剩餘秒數
程式碼:
1. 全域性自定義 Livedata 程式碼
```
class GlobalLivedata : LiveData<String>() {
val coundManager = CountDownManager()
val listener = object : OnDataChangeListener {
override fun change(data: String) {
postValue(data)
}
}
override fun onActive() {
super.onActive()
coundManager.setListener(listener)
}
override fun onInactive() {
super.onInactive()
coundManager.removeListener(listener)
}
companion object {
private lateinit var globalData: GlobalLivedata
fun getInstance(): GlobalLivedata {
globalData = if (::globalData.isInitialized) globalData else GlobalLivedata()
return globalData
}
}
}
複製程式碼
```
2. 倒計時器程式碼較長只貼上一部分,有興趣可以到 github 去檢視完整程式碼
```
private val listeners = mutableListOf<OnDataChangeListener>()
init {
val countDown = object : CountDownTimer(2000 * 1000, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainSecond--
callback("剩餘:${remainSecond} 秒")
}
override fun onFinish() {
callback("倒計時結束")
}
}
countDown.start()
}
/**
* 迴圈遍歷回撥訊息
*/
private fun callback(msg:String) {
for (listener in listeners){
listener.change(msg)
}
}
複製程式碼
```
3. FragmentA、FragmentB 中監聽倒計時狀態
```
GlobalLivedata.getInstance().observe(viewLifecycleOwner,
{ t ->
inflate.findViewById<TextView>(R.id.tv_fragmentA).text = "fragmenta:${t}"
})
複製程式碼
```
```
GlobalLivedata.getInstance().observe(viewLifecycleOwner,
{ t ->
inflate.findViewById<TextView>(R.id.tv_fragmentB).text = "fragmentb:${t}"
})
複製程式碼
```
4. 最終效果
![image](//upload-images.jianshu.io/upload_images/23587538-10d04115653135e9.image?imageMogr2/auto-orient/strip|imageView2/2/w/217/format/webp)
最終效果,當我們切換 Fragment 的時候兩個 Fragment 顯示的秒數是一致的,其實即使我們馬上啟動一個新 activity 去檢視剩餘秒數也是一樣的,有興趣的朋友可以下載 git 程式碼自己嘗試
### 對 Livedata 進行轉換
map 和 switchMap 兩個方法可以對已有的 Livedata 進行轉換得到新的 Livedata
#### Transformation.map
在 activity 中觀察 viewmodel 中的資料更新,當點選 activity 中按鈕的時候會呼叫 viewmodel.sendData 方法傳送資料,然後傳送的資料會做一定的轉換給 activity,然後 activity 列印日誌展示
直接看程式碼吧:
1. 建立 viewmodel,model 中建立 Livedata
```
class TransMapViewModel: ViewModel() {
fun sendData() {
userLivedata.value=User("李白",1200)//對userLivedata進行復制
}
val userLivedata =MutableLiveData<User>()
val mapLiveData = Transformations.map(userLivedata){
"${it.name} : ${it.age}"//這裡可以返回任意型別的資料
}
}
data class User(var name:String,var age:Int)
複製程式碼
```
程式碼中 mapLiveData 是對 userLivedata 進行轉換得到的,所以當我們呼叫 sendData 方法更新 userLivedata 中的方法時,mapLiveData 的回撥也會觸發
2. 在 activity 中觀察 mapLiveData 並點選按鈕傳送小資料
```
mapViewModel.mapLiveData.observe(this,{
logEE(it)
tv_map.text=it
})
btn_map.setOnClickListener {
mapViewModel.sendData()
}
複製程式碼
```
#### Transformation.switchMap
本例中我們實現如下邏輯:
在 activity 中觀察 viewmodel 中的資料更新,當點選 activity 中按鈕的時候會呼叫 viewmodel.sendData 方法傳送資料,然後傳送的資料會做一定的轉換給 activity,然後 activity 列印日誌展示
1. viewmodel 中程式碼
```
class SwitchMapViewModel : ViewModel() {
fun sendData() {
userLivedata.value = SwitchUser("李白", 1200)
}
private val userLivedata = MutableLiveData<SwitchUser>()
val mapLiveData = Transformations.switchMap(userLivedata) {
changeUser(it!!)
}
private fun changeUser(it: SwitchUser): LiveData<String> {
return MutableLiveData("${it.name} 的名字杜甫知道")
}
}
data class SwitchUser(var name: String, var age: Int)
複製程式碼
```
2. 呼叫部分程式碼
```
model.mapLiveData.observe(this, {
logEE(it)
})
btn_switchmap.setOnClickListener {
model.sendData()
}
複製程式碼
```
#### 合併兩個 Livedata(MediatorLiveData)
想象這樣一個場景,您的 app 裡面有一個評論列表的功能,可以對列表內容進行點贊。每一個點贊都是一個非同步任誤,你的產品需求並不想讓使用者點太多贊,比如一分鐘點贊數量不能超過 10 次,這種場景就很適合用 Livedata 的合併功能
我們就不模擬這麼複雜的場景了,我們的例子做這樣一個事情:
介面上有兩個按鈕,點一次相當於點贊一次,我們點選十次按鈕就在介面上展示文字提示使用者已經點選了十次資料。
程式碼展示:
1.model 程式碼
```
class MeditorLiveViewModel : ViewModel() {
var count =0//計數字段
fun setData1(name: String) {
liveData1.value = name
}
fun setData2(age: Int) {
liveData2.value = age
}
private val liveData1 = MutableLiveData<String>()
private val liveData2 = MutableLiveData<Int>()
val liveCombind = MediatorLiveData<String>()
init {
liveCombind.addSource(liveData1) {
increase()
}
liveCombind.addSource(liveData2) {
increase()
}
}
private fun increase() {
count++
if(count==10){
liveCombind.value="安安安安卓同學,您已經點選 ${count}次,再點我也不跟你玩了,收手吧。。。"
}
}
}
複製程式碼
```
model 中建立了三個 Livedata,其中兩個分別是 livedata1 和 livedata2,分別對應其中兩個按鈕。
還有一個 liveCombind 用來回調超過十次呼叫的場景
init 方法中 liveCombind.addSource 呼叫就是表示用來中間攔截 livedata1 和 livedata2 的資料更新,處理 count 累加和是否回撥 liveCombind 的功能
2. activity 中程式碼
```
model.liveCombind.observe(this){
logEE(it)
tv_count.text=it
}
btn_livedata1.setOnClickListener {
model.setData1("李白")
}
btn_livedata2.setOnClickListener {
model.setData2(1000)
}
複製程式碼
```
3. 實現效果
![image](//upload-images.jianshu.io/upload_images/23587538-4b0bc4e3265828bc.image?imageMogr2/auto-orient/strip|imageView2/2/w/217/format/webp)
### observeForever
observeForever 方法也是註冊 Livedata 監聽的方法,表示即使應頁面被覆蓋處於不活躍狀態也可以收到資料改變的回撥
### Livedata 和協程聯合使用
#### emit 方式使用
1. 引入依賴 有時候你可能需要處理非同步任務,任務處理完成後重新整理 ui
這種情況可以使用 Livedata 的擴充套件程式實現
本例我們實現下面的邏輯:
在 viewmodel 中阻塞 4s,然後通知 activity
程式碼:
1. 引入依賴外掛
```
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
複製程式碼
```
2. 開啟非同步任務方法
```
/**
* 開啟非同步任務
*/
fun startAsyncWithSecond(second: Int): LiveData<String> = liveData<String> {
delay(second * 1000L)
emit("倒計時結束")//用來觸發資料回撥
}
複製程式碼
```
當我們呼叫 startAsyncWithSecond 方法的時候會馬上返回一個 Livedata 物件,供我們註冊監聽
3. activity 中註冊 livedata 監聽
```
model.startAsyncWithSecond(3).observe(this){
logEE(it)//model中delay 3s後會返回資料到這裡
}
複製程式碼
```
4. 效果展示
![image](//upload-images.jianshu.io/upload_images/23587538-ccaaa3650e356adc.image?imageMogr2/auto-orient/strip|imageView2/2/w/217/format/webp)
#### emitSource 使用
使用 emitSource 的效果等同於 MediatorLiveData 的效果
我們本例實現如下的效果:
點選按鈕開啟一個 3s 的非同步任務,然後通知 activity 列印日誌。
然後再次開啟一個 3s 的非同步任務,結束後再次通知 activity 列印日誌
程式碼:
1. 建立非同步任務方法
```
fun startAsyncEmitSource(second: Int)= liveData<String> {
delay(second * 1000L)
emit("${second} 秒阻塞完成,再阻塞三秒後通知你")
val emitSourceLivedata = MutableLiveData<String>()
emitSource(
emitSourceLivedata
)
delay(second*1000L)
emitSourceLivedata.value="再次阻塞 ${second}秒完成"
}
複製程式碼
```
2. activity 中註冊監聽
```
model.startAsyncEmitSource(3).observe(this){
logEE(it)
}
複製程式碼
```
3. 效果
![image](//upload-images.jianshu.io/upload_images/23587538-e9839354fd32e5fe.image?imageMogr2/auto-orient/strip|imageView2/2/w/217/format/webp)