iT邦幫忙

2021 iThome 鐵人賽

DAY 20
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 20

day20 在ui蒐集flow,能取代liveData嗎?

好的,前一篇講到了flow可以完全取代liveData,其實錯!!

直接從結論開始講,flow並不支援data binding,也有其限制,用stateflow才能完全取代liveData

flow適用於當數據的開始/停止需要和觀察者匹配

如何正確的從ui蒐集flow

A cold flow backed by a channel or using operators with buffers such as buffer, conflate, flowOn, or shareIn is not safe to collect with some of the existing APIs such as CoroutineScope.launch, Flow< T>.launchIn, or LifecycleCoroutineScope.launchWhenX, unless you manually cancel the Job that started the coroutine when the activity goes to the background.

通過cahnnel產生的flow,或是使用buffer的flow,像是buffer()、conflate()、flowOn、shareIn(),在coroutine.launch、flow.launchIn、lifecycleCoroutineScope.launchwhen...並不能安全地蒐集,除非你在ui進入背景時手動取消job,因為上述的api會持續收集資料,即使ui已經進入背景

/**
 * 錯誤示範
 * */
// Collects from the flow when the View is at least STARTED and
// SUSPENDS the collection when the lifecycle is STOPPED.
// Collecting the flow cancels when the View is DESTROYED.
lifecycleScope.launchWhenStarted {
    locationProvider.locationFlow().collect {
        // New location! Update the map
    } 
}

這種寫法,當ui進入背景,新資料不會被處理,但是某些情況下producer還是會持續進行(blog的範例用channel 的offer),此外,lifecycleScope.launch和launchIn會更危險,即使ui進入背景,仍會不斷處理資料,最終可能導致應用崩潰

//正確寫法
var manuallyCanaelJob:Job?= null
...
manuallyCanaelJob = lifecycleScope.launch{
    locationProvider.locationFlow().collect {
    
    }
}
...
override fun onDestory(){
    manuallyCanaelJob?.cancel()
}

儘管這種寫法正確,但比較麻煩,其實也就一個原因,flow不知道lifecycle

flow不知道lifecycle怎麼辦?

聰明的讀者們,應該從前一篇就有個疑問了吧?flow有collect了為甚麼還要asLiveData呢?是不是他不知道lifecycle呀

沒錯,他就是不知道lifecycle,大家回想一下,應該還記得當年學android為甚麼要用liveData吧,那flow不知道lifecycle不就要我們自己處理,好麻煩好麻煩,還是用asLiveData好了

且慢,其實~已經寫好囉
首先,確定gradle版本 androidx.lifecycle:lifecycle-*:2.4.0-alpha03

要在alpha01後的才有repeatOnLifecycle

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-beta01"

這裡看新版本

lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
        //只有在lifecycle到start及start之後

    }
}

注意:
The minCompileSdk (31) specified in a
dependency's AAR metadata (META-INF/com/android/build/gradle/aar-metadata.properties)
is greater than this module's compileSdkVersion (android-30).
Dependency: androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-beta01.

對android compile版本也有要求,最低31
gradle>android compileSdk 最低要31

repeatOnLifecycle

他是一個suspend方法,會接受一個生命周期的狀態作為參數,當生命週期到該狀態時,會建立一個coroutine,並執行區塊中的代碼; 而當生命週期低於該狀態時,會自動取消coroutine

如此一來,repeatOnLifecycle就能為我們管理collect的取消,正如大多數函式,建議在activity的onCreat或fragment的onviewCreate中使用

fragment

Important: Fragments should always use the viewLifecycleOwner to trigger UI updates. However, that’s not the case for DialogFragments which might not have a View sometimes. For DialogFragments, you can use the lifecycleOwner.

因為這個repeatOnLifecycle現在還蠻新的,這三種寫法都可以編譯過,目前是沒發現什麼差異,但有被講到,還是提一下

lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED)
//or
repeatOnLifecycle(Lifecycle.State.STARTED)
//or
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED)

官方好圖

解釋了差異

給個例子


那寫一個小型範例

//retrofit
@GET("posts/{num}")
    suspend fun getPostFlow(
        @Path ("num") num: Int
    ): Post
// repo
val postFlow: Flow<Post> = flow {
    var count = 1
    while (true){
        val result = service.getPostFlow(count)
        emit(result)
        count++
        delay(2000L)
    }
}
// viewModel
val changeId = repo.postFlow
//fragment
lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
        viewModel.changeId.collect{
            binding.apiResultText.text = it.toString()
        }
        
        viewModel.otherFlow.collect{
        //do something
        }
    }
}

應該和liveData的內容一樣很好理解,那repeatOnLifecycle其實就是告訴開發者,每次ui走到哪個生命週期後,這個block會被調用

google爸爸說封裝一下好了

於是...

lifecycleScope.launch {
    viewModel.changeId
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect{

        }
}

對的,非常簡單,但google爸爸說,這個用法有兩點要注意

  1. Operators applied before the flowWithLifecycle operator will be cancelled when the lifecycle is below minActiveState.
  2. only for one flow collect
  3. repeatOnLifecycle must be used with the viewLifecycleOwner in Fragments

https://miro.medium.com/max/2000/1*fmQRBPMPpnO7NAO2bg0GKw.

為什麼用flow不用channel

記得我們之前講過channel吧,透過send()和receive()可以在不同corouitne之前傳遞值,而flow透過emit()和collect()發送流,兩個看起來~ 阿不就差不多

錯了錯了,從設計原因來看channel是為了同步設計的,而flow是為了數據流而設計的,也因為設計的實現不同,他們會有各自適合的工作,留個地方放之後寫比較的連結

而flow在設計上考量到許多數據流的痛點,也為此設計了很多操作符,更重要的是,flow是透過終端操作符來啟動數據留

close flow

flow分三個部分,producer、intermediary、consumer,consumer會啟動流,而當producer的代碼執行完畢、出現Exception、或是consumer停止,便會關閉數據流

因此flow比起channel更難在producer端出現異常(blog寫說不會或是非常難),而channel如果沒有正常關閉,會在prioducer端浪費資源

我們之前講過flow是一個冷數據流,他必須有人呼叫consumer的方法才會執行,而每次呼叫,都會創造出一個新的數據流

一個永遠不會被suspend的流,永遠不會被取消

choose between flow and broadcastChannel

何時用broadcastChannel
當producer和consumer在不同lifecycle或是完全獨立存在時

broadcastChannel是基於Channel的實現,他能讓producer基於不同的lifecycle,並且廣播給任何監聽他的對象,而他的producer就不會每次都重新啟動

注意,如果用broadcastChannel.asFlow(),轉換為數據流,這時關閉flow並不會取消訂閱/觀察broadcastChannel,此時資源仍處在活耀狀態,直到vroadcastChannel取消或關閉。而關閉後只能在創造新的實例

更新,在shareFlow的文件哩,講明了會取代他,所以我就不講了

能取代liveData嗎?

看到這裡應該會發現,已經沒有liveData了,沒錯,只要flow也能和lifecycle合作,就能夠一定程度上取代liveData,注意,只有一定程度

限制

  1. kotlin限定
  2. flow不支援data binding(之後的文章會講道)
  3. 無法透過value取最新值(Flow is stateless (no .value access).)
  4. 每個collect都會創建新的實例,這代表在model層的操作會是重複的

這其實不是flow的缺點,而是特性,在某些狀態下他會非常實用

優點

  1. 有更多的操作符
  2. 方便的切換thread

那不用databinding的開發者,選哪個livedata還是flow呢?

看code reviewer有沒有要求,現在repeatOnLifecycle()還在alpha階段,之後可能還會有變動,但可以一邊用asLiveData,一邊了解新寫法

p.s.今天時間比較趕,應該有遺漏細節,之後會回來捕,或是你們直接留言告訴我哈哈哈

連結

必看

lessons-learnt-using-coroutines-flow
a-safer-way-to-collect-flows-from-android-uis
repeatonlifecycle-api-design-story
coroutines-best-practices
Substituting Android’s LiveData: StateFlow or SharedFlow?


上一篇
day19 Kotlin coroutine flow with liveData in MVVM
下一篇
day21 開分支,淺談kotlin paging3 with flow
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言