1. 程式人生 > >Coroutines in Android - One Shot and Multiple Values

Coroutines in Android - One Shot and Multiple Values

Coroutines in Android - One Shot and Multiple Values

在Android中, 我們用到的資料有可能是一次性的, 也有可能是需要多個值的.

本文介紹Android中結合協程(coroutines)的MVVM模式如何處理這兩種情況. 重點介紹協程Flow在Android中的應用.

One-shot vs multiple values

實際應用中要用到的資料可能是一次性獲取的(one-shot), 也可能是多個值(multiple values), 或者稱為流(stream).

舉例, 一個微博應用中:

  • 微博資訊: 請求的時候獲取, 結果返回即完成. -> one-shot.
  • 閱讀和點贊數: 需要觀察持續變化的資料來源, 第一次結果返回並不代表完成. -> multiple values, stream.

MVVM構架中的資料型別

一次性操作和觀察多個值(流)的資料, 在架構上看起來會有什麼不同呢?

  • One-shot operation: ViewModel中是LiveData, Repository和Data source中是suspend fun.
class MyViewModel {
    val result = liveData {
        emit(repository.fetchData())
    }
}

多個值的實現有兩種選擇:

  • Multiple values with LiveData: ViewModel, Repository, Data source都返回LiveData. 但是LiveData其實並不是為流式而設計的, 所以用起來會有點奇怪.
  • Streams with Flow: ViewModel中是LiveData, Repository和Data source返回Flow.

可以看出兩種方式的主要不同點就是ViewModel消費的資料形式, 是LiveData還是Flow.

後面會從ViewModel, Repository和Data source三個層面來說明.

Flow是什麼

既然提到了Flow

, 我們先來簡單講一下它是什麼, 這樣大家能在same page.

Kotlin中的多個值, 可以儲存在集合中, 比如list, 也可以靠計算生成sequence, 但如果值是非同步生成的, 需要將方法標記為suspend來避免阻塞主執行緒.

flow和sequence類似, 但flow是非阻塞的.

看這個例子:

fun foo(): Flow<Int> = flow {
    // flow builder
    for (i in 1..3) {
        delay(1000) // pretend we are doing something useful here
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> {
    // Launch a concurrent coroutine to check if the main thread is blocked
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(1000)
        }
    }
    // Collect the flow
    foo().collect { value -> println(value) }
}

這段程式碼執行後輸出:

I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3
  • 這裡用來構建Flow的flow方法是一個builder function, 在builder block裡的程式碼可以被suspend.
  • emit方法負責傳送值.
  • cold stream: 只有呼叫了terminal operation才會被啟用. 最常用的是collect().

如果熟悉Reactive Streams, 或用過RxJava就可以感覺到, Flow的設計看起來很類似.

ViewModel層

傳送單個值的情況比較簡單和典型, 這裡不再多說, 主要說傳送多個值的情況. 每次又分ViewModel消費的型別是LiveData還是Flow兩種情況來討論.

發射N個值

LiveData -> LiveData

val currentWeather: LiveData<String> = dataSource.fetchWeather()

Flow -> LiveData

val currentWeatherFlow: LiveData<String> = liveData {
    dataSource.fetchWeatherFlow().collect {
        emit(it)
    }
}

為了減少boilerplate程式碼, 簡化寫法:

val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData()

後面都直接用這種簡化的形式了.

發射1+N個值

LiveData -> LiveData

val currentWeather: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(dataSource.fetchWeather())
}

emitSource()傳送的是一個LiveData.

Flow -> LiveData

Flow的時候可以用上面同樣的形式:

val currentWeatherFlow: LiveData<String> = liveData {
    emit(LOADING_STRING)
    emitSource(
        dataSource.fetchWeatherFlow().asLiveData()
    )
}

這樣寫看起來有點奇怪, 可讀性不好, 所以可以利用Flow的API, 寫成這樣:

val currentWeatherFlow: LiveData<String> = 
dataSource.fetchWeatherFlow()
    .onStart{emit(LOADING_STRING)}
    .asLiveData()

Suspend transformation

如果想在ViewModel中做一些轉換.

LiveData -> LiveData

val currentWeatherLiveData: LiveData<String> = dataSource.fetchWeather().switchMap {
    liveData { emit(heavyTransformation(it)) }
    
}

這裡不太適合用map來做轉換, 因為是在主執行緒.

Flow -> LiveData

Flow來做轉換就很方便:

val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow()
    .map{ heavyTransformation(it) }
    .asLiveData()

Repository層

Repository層通常用來組裝和轉換資料.
LiveData被設計的初衷並不是做這些轉換的.
Flow則提供了很多有用的操作符, 所以顯然是一種更好的選擇:

val currentWeatherFlow: Flow<String> =
    dataSource.fetchWeatherFlow()
        .map { ... }
        .filter { ... }
        .dropWhile { ... }
        .combine { ... }
        .flowOn(Dispatchers.IO)
        .onCompletion { ... }

Data Source層

Data Source層是網路和資料庫, 通常會用到一些第三方的庫.
如果用了支援協程的庫, 如Retrofit和Room, 那麼只需要把方法標記為suspend的, 就行了.

  • Retrofit supports coroutines from 2.6.0
  • Room supports coroutines from 2.1.0

One-shot operations

對於一次性操作比較簡單, 資料層的只要suspend方法返回值就可以了.

suspend fun doOneShot(param: String) : String = retrofitClient.doSomething(param)

如果所用的網路或者資料庫不支援協程, 有辦法嗎? 答案是肯定的.
suspendCoroutine來解決.

比如你用的第三方庫是基於callback的, 可以用suspendCancellableCoroutine來改造one-shot operation:

suspend fun doOneShot(param: String): Result<String> = 
suspendCancellableCoroutine { continuation -> 
    api.addOnCompleteListener { result -> 
        continuation.resume(result)
    }.addOnFailureListener { error -> 
        continuation.resumeWithException(error)
    }.fetchSomething(param)
}

如果協程被取消了, 那麼resume會被忽略.

驗證程式碼如期工作後, 可以做進一步的重構, 把這部分抽象出來.

Data source with Flow

資料層返回Flow, 可以用flow builder:

fun fetchWeatherFlow(): Flow<String> = flow {
    var counter = 0
    while(true) {
        counter++
        delay(2000)
        emit(weatherConditions[counter % weatherConditions.size])
    }
}

如果你所用的庫不支援Flow, 而是用回撥, callbackFlow builder可以用來改造流.

fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
    val callback = object: Callback {
        override fun onNextValue(value: T) {
            offer(value)
        }
        
        override fun onApiError(cause: Throwable) {
            close(cause)
        }
        
        override fun onCompleted() = close()
    }
    api.register(callback)
    awaitClose { api.unregister(callback) }
}

可能並不需要LiveData

在上面的例子中, ViewModel仍然保持了自己向UI暴露的資料是LiveData型別. 那麼有沒有可能不用LiveData呢?

lifecycleScope.launchWhenStarted {
    viewModel.flowToFlow.collect {
        binding.currentWeather.text = it
    }
}

這樣其實和用LiveData是一樣的效果.

參考

視訊:

  • LiveData with Coroutines and Flow (Android Dev Summit '19)

文件:

  • Kotlin官方文件: Flow

部落格:

  • Coroutines On Android (part III): Real work
  • Lessons learnt using Coroutines Flow in the Android Dev Summit 2019 app

最後, 歡迎關注微信公眾號: 聖騎士Wind

相關推薦

Coroutines in Android - One Shot and Multiple Values

Coroutines in Android - One Shot and Multiple Values 在Android中, 我們用到的資料有可能是一次性的, 也有可能是需要多個值的. 本文介紹Android中結合協程(coroutines)的MVVM模式如何處理這兩種情況. 重點介紹協程Flow在Andr

Android Developers Blog: Moar Power in Android 9 Pie and the future

Posted by Madan Ankapura, Product Manager, Android This is the first in a series of blog posts that outline strategies and guidance in Android with

解決author波浪線Spellchecker inspection helps locate typos and misspelling in your code, comments and literals, and fix them in one click

博客 翻譯 cli 修復 and idea tro alt 拼寫檢查 自從把默認的頭註釋的author改成自己的名字以後越看越順眼,但是發現名字下面一直有個波浪線,強迫癥簡直不能忍。 然後當你把鼠標放上去,再點擊提示上的“more”,會看到下面的提示: Spellchec

JNI and Reflection in Android

http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/package-summary.html JNI: JNI函式其實和定義該JNI函式的類的其它函式沒有什麼區別,是屬於該類的一個函式而已,只是是由原生代碼實現的,所以JNI函式訪

How To Communicate Between Fragments and Activities in Android

Welcome to: how to communicate between fragments and activities in your android applications. If you have not done so already, please check out my previ

Simple and Multiple Linear Regression in Python

Linear Regression in PythonThere are two main ways to perform linear regression in Python — with Statsmodels and scikit-learn. It is also possible to use t

Kotlin for Android (III): Extension functions and default values

Now that you know the basics about Kotlin and how to configure your project, it´s time to talk about some interesting things that Kotlin can do for u

Property delegation in Kotlin: Assign values in Android without having the context (KAD 15)

As we’ve seen in previous articles, properties need a default value, they can’t be declared without assigning them a value. This is a problem, becaus

"Loading a plug-in failed The plug-in or one of its prerequisite plug-ins may be missing or damaged and may need to be reinstal

The Unarchiver 雖好,但存在問題比我們在mac上zip打包一個軟體xcode, 然後copy to another mac, 這時用The Unarchiver解壓縮出來的xcode包不能執行, 好像是裡面的檔案資訊結構被破壞,會出現而用archive utility 解壓就能正常執行。  通

解決mysql報錯:- Expression #1 of ORDER BY clause is not in GROUP BY clause and contains nonaggregated column 'information_schema.PROFILING.SEQ'

_for tran contains column schema mysql eat table express mysql執行報錯: - Expression #1 of ORDER BY clause is not in GROUP BY clause and cont

android adb shell and monkey 學習記錄

型號 orm -a reboot 觸摸事件 serial roc ear cpu Monkey環境: android SDK and JDK SDK目錄下的platform-tools和tools目錄要配置環境變量 查看版本: ADB 的安裝這裏就

UDEV SCSI Rules Configuration for ASM in Oracle Linux 5 and 6

instance bus works article rtp fine with rul con UDEV SCSI Rules Configuration for ASM in Oracle Linux 5 and 6 For Oracle Automatic Sto

The method Inflate() in android

androi eth height between _id sed dial font views Inflate() method can find out a layout defined by xml,as like the findViewById() me

ionic3打包出錯ionic cordova build android(系列一):could not find an installed version of gradle either in android studio

lan 問題 打包 fail .html ascii failed contains ref 1.運行ionic cordova build android 時報錯:could not find an installed version of gradle either i

Common Words Used in Android

device ive rate ict ren item cau ble span retrieve to get something back scrap to discard or discontinue something because it is consid

What is “passive data structure” in Android/Java?

nag holding bstr say roi containe ive ces get From the Android developer web link: http://developer.android.com/reference/android/content

【MySQL】出現“SELECT list is not in GROUP BY clause and contains xxx”錯誤提示

engine 數據 連接 設置 subst sql_mode xxx and sele 需要設置為 sql_mode=‘NO_ENGINE_SUBSTITUTION‘ 設置方式: 1、MySQL客戶端連接上數據庫。 2、當前操作的數據切換成 mysql 3、執行 SELEC

vue初級頁面 tab切換 表格動態綁定 v-for(item,index) in datas.system :value='item.values'

base mode list blog 技術 mod bin on() :focus <?php /** * Created by PhpStorm. * User: 信息部 * Date: 2017/10/26 * Time: 13:10 */ u

Cloud in Action:Practice Docker and its Networking

docker lxc namespaceCloud in Action: Practice Docker and its Networking on Ubuntu 薛國鋒 [email protected] VM, LXC, Docker and Libcontainer VMs

安裝Android studio出現'tools.jar' seems to be not in Android Studio classpath......的解決方法

eas 一個 origin java_home ems view 使用 分享 title 安裝Android studio出現‘tools.jar‘ seems to be not in Android Studio classpath......的解決方法 原創 201