iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 7
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 7

[Day 7] Android Architecture Components:LiveData

  • 分享至 

  • xImage
  •  

今天來説說另一個跟 ViewModel 息息相關的東西 --- LiveData

LiveData 是什麼

LiveData 與 ViewModel 一樣,是一個可以感知 Activity / Fragment 生命週期的 Data Holder,因此他可以確保只在元件處在 "Active" 才會更新 UI , View 在背景時則會保存此狀態,並在下一次元件甦醒時更新畫面,而 View 被摧毀時則會一併被回收,從而避免了 memory leak。

想想以前在把資料顯示在 Activity 上時,動不動就要檢查 Activity 是否存活,如今有了 LiveData 後就可以省略這些步驟了。

基本使用

dependencies {
    def lifecycle_version = "2.1.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
}

如果昨天已經加入的話,這裡可以省略。

首先在 ViewModel 中加初始化一個 LiveData,並寫一個傳送資料的方法:

class TaskViewModel(private val repository: TasksRepository) : ViewModel() {

    private val _dataLoading = MediatorLiveData<Boolean>()
    val dataLoading: LiveData<Boolean>
        get() = _dataLoading
    
    fun loadData() {
        _dataLoading.value = true
        // 等一段時間後改變資料
        Handler().postDelayed({
            _dataLoading.value = false
        }, 2500)
    }
    ......
}

dataLoading 之所以這麼寫是為了確保在 UI 層操作 LiveData 時只能夠負責顯示資料,而不能保存資料狀態。
接著在 Activity 觀察(observe) LiveData:

class MainActivity : AppCompatActivity() {

    private val repository by lazy { TasksRepository() }

    private val factory by lazy { TodoViewModelFactory(repository) }

    private lateinit var viewModel: TaskViewModel

    ......

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProviders.of(this, factory).get(TaskViewModel::class.java)

        viewModel.dataLoading.observe(this, Observer {
            if (it) {
                Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "Done!", Toast.LENGTH_SHORT).show()
            }
        })

        btnLiveData.setOnClickListener {
            viewModel.loadData()
        }

        ......
    }

    ......
}

還有另一種觀察 LiveData 的方式是配合 DataBinding ,也是比較常見的做法,但是這邊先省略,之後幾天再詳細介紹。

通常我們會在 onCreate() 開始觀察 LiveData ,有以下的原因:

  1. 確保系統不會因為 onResume() 而有多餘的調用。
  2. 確保 Activity / Fragment 在 Active 狀態時擁有可顯示的資料。

一般而言 LiveData 只會在資料改變時傳遞給觀察者,但是有例外狀況如昨天在 ViewModel 提到類似的例子:如果旋轉螢幕則因為 View 重新初始化,所以又會收到 LiveData 的資料,造成 Toast UI 重複顯示等。

Google 為此提出了一種解法,使用一個封裝類封裝 LiveData 的資料,封裝類內部在發現資料重複發送時即阻止這一次的畫面更新,具體做法如下:

open class Event<out T>(private val content: T) {
    // 一個用來標示這個資料是否已更新 UI 的 flag
    var hasBeenHandled = false
        private set

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    fun getContent(): T = content
}

/**
 * 拓展使用 [Event] 時的 [Observer] 操作行為
 */
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit)
    : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.getContentIfNotHandled()?.let {
            onEventUnhandledContent(it)
        }
    }
}

接著修改 Activity 的訂閱方式以及 ViewModel 傳遞的資料類型:

class TaskViewModel(private val repository: TasksRepository) : ViewModel() {

    private val _dataLoading = MediatorLiveData<Event<Boolean>>()
    val dataLoading: LiveData<Event<Boolean>>
        get() = _dataLoading

    fun loadData() {
        _dataLoading.value = Event(true)

        Handler().postDelayed({
            _dataLoading.value = Event(false)
        }, 2500)
    }
    ......
}

class MainActivity : AppCompatActivity() {

    private val repository by lazy { TasksRepository() }

    private val factory by lazy { TodoViewModelFactory(repository) }

    private lateinit var viewModel: TaskViewModel

    ......

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProviders.of(this, factory).get(TaskViewModel::class.java)

        viewModel.dataLoading.observe(this, EventObserver {
            if (it) {
                Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "Done!", Toast.LENGTH_SHORT).show()
            }
        })

        btnLiveData.setOnClickListener {
            viewModel.loadData()
        }

        ......
    }

    ......
}

和 Room 一起共同使用

現在 Room 也可以直接返回 LiveData 類,好處是 observe 後,當 DB 資料更新時也會一併更新 LiveData ,可以讓我們很簡單就讓 UI 顯示的資料與 DB 裡的資料保持一致。

Transform LiveData

有時候在 LiveData 被訂閱前可能需要把 LiveData 裡的資料轉成另一個型態,或是需要基於某個 LiveData 實體的值回傳不同的 LiveData ,這時候就會需要 Transformations 來實現。

Transformations.map()

類似 Kotlin / RxJava 的 map ,即把 LiveData 裡的資料轉成另一個型態並送出:

val taskLiveData: LiveData<Task> = ......

val taskTitleLiveData: LiveData<String> = Transformations.map(taskLiveData) {
    "Title: " + it.title
}

Transformations.switchMap()

如果有一個情境,

搜尋某筆資料, return 一個 LiveData

實作方法如下:

fun getTask(id: String): LiveData<Task> {
    return dao.getTask(id)
}

上面的方法在 View 訂閱後可以發出我們需要的資料,但是當我們搜尋不同 id 的 Task ,會回傳不同的 LiveData 回來,讓我們必須重新在訂閱一次,這時候就可以使用 switchMap() 處理,讓我們修改一下程式碼:

val idLiveData: MutableLiveData<String> = ......
val taskTitleLiveData: LiveData<Task> = MutableLiveData()
init {
    taskTitleLiveData = Transformations.switchMap(idLiveData) {
        getTask(it)
    }
}

fun searchTask(id: String) {
    idLiveData.value = id
}
        
fun getTask(id: String): LiveData<Task> {
    return ......
}

這樣就可以根據別的 LiveData 發射的 trigger 接收資料的同時,使用同一個 LiveData instance 訂閱 LiveData。

合併多個 LiveData

如果有個需求:

local 或是網路的資料有更新時,需要通知 UI

這時候就可以在 local 及 network 各自建立一個 LiveData ,並將兩個資源合併到 MediatorLiveData 中。

如此一來,當 local 及 network 有變化時,只需要訂閱 MediatorLiveData 即可知道兩個來源的資料更新。

LiveData 的介紹先到這邊,其實還有許多功能沒有提到,這邊只是介紹了一些較常見的部分,明天再來説説另一個有趣的 Component。


上一篇
[Day 6] Android Architecture Components:ViewModel
下一篇
[Day 8] Navigation Component:Part 1
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言