目的
本文不涉及 Flow 很深的東西辟犀,即使不會 Flow 也可以上手使用械蹋。
話接上篇文章 兩種方式封裝Retrofit+協(xié)程出皇,實現(xiàn)優(yōu)雅快速的網(wǎng)絡(luò)請求
最近在獨立寫一個新的項目,用的是封裝二朝蜘,雖然幾行代碼就可以進行網(wǎng)絡(luò)請求,但是在使用過程中還是覺得有點遺憾涩金,寫起來也不是非称状迹快捷暇仲,存在模板代碼。
加上很多小伙伴想要一個Flow版本的副渴,忙里偷閑奈附,用kotlin Flow對這套框架進行了優(yōu)化,發(fā)現(xiàn)flow真香煮剧。
一斥滤、以前封裝的遺憾點
主要集中在如下2點上:
Loading的處理
多余的LiveData
總而言之,就是需要寫很多模板代碼勉盅。
不必編寫模版代碼的一個最大好處就是: 寫的代碼越少佑颇,出錯的概率越小.
1.1 Loading的處理
對于封裝二,雖然解耦比封裝一更徹底草娜,但是關(guān)于Loading這里我覺得還是有遺憾挑胸。
試想一下:如果Activity中業(yè)務(wù)很多、邏輯復(fù)雜宰闰,存在很多個網(wǎng)絡(luò)請求茬贵,在需要網(wǎng)絡(luò)請求的地方都要手動去showLoading()
,然后在 observer()
中手動調(diào)用 stopLoading()
。
假如Activity中代碼業(yè)務(wù)復(fù)雜移袍,存在多個api接口解藻,這樣Activity中就存在很多個與loading有關(guān)的方法。
此外葡盗,如果一個網(wǎng)絡(luò)請求的showLoading()
方法和dismissLoading()
方法相隔很遠螟左。會導(dǎo)致一個順序流程的割裂。
請求開始前showLoading()
---> 請求網(wǎng)絡(luò) ---> 結(jié)束后stopLoading()
戳粒,這是一個完整的流程路狮,代碼也應(yīng)該盡量在一起,一目了然蔚约,不應(yīng)該割裂存在奄妨。
如果代碼量一多,以后維護起來苹祟,萬一不小心刪除了某個showLoading()
或者stopLoading()
砸抛,也容易導(dǎo)致問題。
還有就是每次都要手動調(diào)用這兩個方法树枫,麻煩直焙。
1.2 重復(fù)的LiveData聲明
個人認為常用的網(wǎng)絡(luò)請求分為兩大類:
用完即丟,只運行一次砂轻,返回一個結(jié)果
需要監(jiān)聽數(shù)據(jù)變化奔誓,可以在一段時間內(nèi)發(fā)出多個值
舉個常見的例子,看下面這個頁面:
用戶一進入這個頁面搔涝,綠色框里面內(nèi)容基本不會變化厨喂,(不去糾結(jié)微信這個頁面是不是webview之類的)和措,這種ui其實是不需要設(shè)置一個LiveData去監(jiān)聽的,因為它幾乎不會再更新了蜕煌。
典型的還有:點擊登錄按鈕派阱,成功后就進去了下一個頁面。
但是紅色的框里面的ui不一樣斜纪,需要實時刷新數(shù)據(jù)贫母,也就用到LiveData監(jiān)聽,這種情況下觀察者訂閱者模式的好處才真正展示出來盒刚。并且從其他頁面過來腺劣,LiveData也會把最新的數(shù)據(jù)自動更新。
對于用完即丟的網(wǎng)絡(luò)請求伪冰,LoginViewModel
會存在這種代碼:
// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>()
并且對應(yīng)的Activity中也需要監(jiān)聽這3個LiveData誓酒。
這種模板代碼讓我寫的很煩。
用了Flow優(yōu)化后贮聂,完美的解決這2個痛點靠柑。
“Talk is cheap. Show me the code.”
二、集成Flow之后的用法
2.1 請求自帶Loading&&不需要監(jiān)聽數(shù)據(jù)變化
需求:
不需要監(jiān)聽數(shù)據(jù)變化吓懈,對應(yīng)上面的用完即丟
不需要在ViewModel中聲明LiveData成員對象
發(fā)起請求之前自動
showLoading()
歼冰,請求結(jié)束后自動stopLoading()
類似于點擊登錄按鈕,finish 當(dāng)前頁面耻警,跳轉(zhuǎn)到下一個頁面
TestActivity
中示例代碼:
// TestActivity.kt
private fun login() {
launchWithLoadingAndCollect({mViewModel.login("username", "password")}) {
onSuccess = { data->
showSuccessView(data)
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onError = {e ->
e.printStackTrace()
}
}
}
TestViewModel
中代碼:
// TestViewModel中代碼
suspend fun login(username: String, password: String): ApiResponse<User?> {
return repository.login(username, password)
}
2.2 請求不帶Loading&&不需要聲明LiveData
需求:
不需要監(jiān)聽數(shù)據(jù)變化
不需要在ViewModel中聲明LiveData成員對象
不需要Loading的展示
// TestActivity.kt
private fun getArticleDetail() {
launchAndCollect({ mViewModel.getArticleDetail() }) {
onSuccess = {
showSuccessView()
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onDataEmpty = {
showEmptyView()
}
}
}
TestViewModel
中代碼和上面一樣隔嫡,這里就不寫了。
是不是非常簡單甘穿,一個方法搞定腮恩,將Loading的邏輯都隱藏了,再也不需要手動寫 showLoading()
和 stopLoading()
温兼。
并且請求的結(jié)果直接在回調(diào)里面接收秸滴,直接處理,這樣請求網(wǎng)絡(luò)和結(jié)果的處理都在一起募判,看起來一目了然荡含,再也不需要在 Activity
中到處找在哪監(jiān)聽的 LiveData
。
同樣届垫,它跟 LiveData
一樣释液,也會監(jiān)聽 Activity
的生命周期,不會造成內(nèi)存泄露装处。因為它是運行在Activity
的 lifecycleScope
協(xié)程作用域中的误债。
2.3 需要監(jiān)聽數(shù)據(jù)變化
需求:
需要監(jiān)聽數(shù)據(jù)變化,要實時更新數(shù)據(jù)
需要在 ViewModel 中聲明 LiveData 成員對象
例如實時獲取最新的配置、最新的用戶信息等
TestActivity
中示例代碼:
// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {
private fun initObserver() {
mViewModel.wxArticleLiveData.observeState(this) {
onSuccess = { data: List<WxArticleBean>? ->
showSuccessView(data)
}
onDataEmpty = { showEmptyView() }
onFailed = { code, msg -> showFailedView(code, msg) }
onError = { showErrorView() }
}
}
private fun requestNet() {
// 需要Loading
launchWithLoading {
mViewModel.requestNet()
}
}
}
ViewModel
中示例代碼:
class ApiViewModel : ViewModel() {
private val repository by lazy { WxArticleRepository() }
val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()
suspend fun requestNet() {
wxArticleLiveData.value = repository.fetchWxArticleFromNet()
}
}
本質(zhì)上是通過FLow來調(diào)用LiveData
的setValue()
方法,還是LiveData
的使用。雖然可以完全用 Flow 來實現(xiàn)辜窑,但是我覺得這里用 Flow 的方式麻煩旺垒,不容易懂,還是怎么簡單怎么來形帮。
這種方式其實跟上篇文章中的封裝二差不多槽惫,區(qū)別就是不需要手動調(diào)用Loading
有關(guān)的方法。
用2張流程圖來對比下上面的方式:
[圖片上傳失敗...(image-25b035-1638674349543)]
三辩撑、拆封裝
如果不抽取通用方法是這樣寫的:
// TestActivity.kt
private fun login() {
lifecycleScope.launch {
flow {
emit(mViewModel.login("username", "password"))
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}.collect { response ->
when (response) {
is ApiSuccessResponse -> showSuccessView(response.data)
is ApiEmptyResponse -> showEmptyView()
is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
is ApiErrorResponse -> showErrorView(response.error)
}
}
}
}
簡單介紹下Flow
:
Flow
類似于RxJava
,操作符都跟Rxjava
差不多界斜,但是比Rxjava
簡單很多,kotlin
通過flow
來實現(xiàn)順序流和鏈?zhǔn)骄幊獭?/p>
flow
關(guān)鍵字大括號里面的是方法的執(zhí)行合冀,結(jié)果通過emit
發(fā)送給下游各薇。
onStart
表示最開始調(diào)用方法之前執(zhí)行的操作,這里是展示一個 loading ui
君躺;
onCompletion
表示所有執(zhí)行完成峭判,不管有沒有異常都會執(zhí)行這個回調(diào)。
collect
表示執(zhí)行成功的結(jié)果回調(diào)棕叫,就是emit()
方法發(fā)送的內(nèi)容林螃,flow
必須執(zhí)行collect
才能有結(jié)果。因為是冷流俺泣,對應(yīng)的還有熱流疗认。
更多的Flow知識點可以參考其他博客和官方文檔。
這里可以看出伏钠,通過Flow完美的解決了loading的顯示與隱藏横漏。
我這里是在Activity
中都調(diào)用flow
的流程,這樣我們擴展BaseActivity
即可熟掂。
為什么擴展的是BaseActivity
?
因為startLoading()
和stopLoading()
在BaseActivity
中缎浇。??
3.1 解決 flow 的 Loading 模板代碼
fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend () -> ApiResponse<T>): Flow<ApiResponse<T>> {
return flow {
emit(block())
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}
}
這樣每次調(diào)用launchWithLoadingGetFlow
方法,里面就實現(xiàn)了 Loading 的展示與隱藏打掘,并且會返回一個 FLow 對象华畏。
下一步就是處理 flow 結(jié)果collect
里面的模板代碼。
3.2 聲明結(jié)果回調(diào)類
class ResultBuilder<T> {
var onSuccess: (data: T?) -> Unit = {}
var onDataEmpty: () -> Unit = {}
var onFailed: (errorCode: Int?, errorMsg: String?) -> Unit = { _, _ -> }
var onError: (e: Throwable) -> Unit = { e -> }
var onComplete: () -> Unit = {}
}
各種回調(diào)按照項目特性刪減即可尊蚁。
3.3 對ApiResponse對象進行解析
private fun <T> parseResultAndCallback(response: ApiResponse<T>,
listenerBuilder: ResultBuilder<T>.() -> Unit) {
val listener = ResultBuilder<T>().also(listenerBuilder)
when (response) {
is ApiSuccessResponse -> listener.onSuccess(response.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
is ApiErrorResponse -> listener.onError(response.throwable)
}
listener.onComplete()
}
上篇文章這里的處理用的是繼承LiveData
和Observer
亡笑,這里就不需要了,畢竟繼承能少用就少用横朋。
3.4 最終抽取方法
將上面的步驟連起來如下:
fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend () -> ApiResponse<T>,
listenerBuilder: ResultBuilder<T>.() -> Unit) {
lifecycleScope.launch {
launchWithLoadingGetFlow(block).collect { response ->
parseResultAndCallback(response, listenerBuilder)
}
}
}
3.5 將Flow轉(zhuǎn)換成LiveData對象
獲取到的是Flow
對象仑乌,如果想要變成LiveData
,Flow
原生就支持將Flow
對象轉(zhuǎn)換成不可變的LiveData
對象。
val loginFlow: Flow<ApiResponse<User?>> =
launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()
調(diào)用的是 Flow 的asLiveData()
方法晰甚,原理也很簡單,就是用了livedata
的擴展函數(shù):
@JvmOverloads
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}
這里返回的是LiveData<ApiResponse<User?>>
對象衙传,如果想要跟上篇文章一樣用StateLiveData
,在observe
的回調(diào)里面監(jiān)聽不同狀態(tài)的callback
。
以前的方式是繼承厕九,有如下缺點:
- 必須要用
StateLiveData
蓖捶,不能用原生的LiveData
,侵入性很強 - 不只是繼承
LiveData
,還要繼承Observer
扁远,麻煩 - 為了實現(xiàn)這個俊鱼,寫了一堆的代碼
這里用 Kotlin 擴展實現(xiàn),直接擴展 LiveData
:
@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
owner: LifecycleOwner,
listenerBuilder: ResultBuilder<T>.() -> Unit
) {
val listener = ResultBuilder<T>().also(listenerBuilder)
observe(owner) { apiResponse ->
when (apiResponse) {
is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
is ApiErrorResponse -> listener.onError(apiResponse.throwable)
}
listener.onComplete()
}
}
感謝Flywith24開源庫提供的思路畅买,感覺自己有時候還是在用Java的思路在寫Kotlin并闲。
3.6 進一步完善
很多網(wǎng)絡(luò)請求的相關(guān)并不是只有 loading 狀態(tài),還需要在請求前和結(jié)束后處理一些特定的邏輯谷羞。
這里的方式是:直接在封裝方法的參數(shù)加 callback帝火,默認用是 loading 的實現(xiàn)。
fun <T> BaseActivity.launchAndCollect(
requestBlock: suspend () -> ApiResponse<T>,
startCallback: () -> Unit = { showLoading() },
completeCallback: () -> Unit = { dismissLoading() },
listenerBuilder: ResultBuilder<T>.() -> Unit
)
四湃缎、針對多數(shù)據(jù)來源
雖然項目中大部分都是單一數(shù)據(jù)來源犀填,但是也偶爾會出現(xiàn)多數(shù)據(jù)來源,多數(shù)據(jù)源結(jié)合Flow的操作符嗓违,也非常的方便宏浩。
示例
假如同一份數(shù)據(jù)可以從數(shù)據(jù)庫獲取,可以從網(wǎng)絡(luò)請求獲取靠瞎,TestRepository
的代碼如下:
// TestRepository.kt
suspend fun fetchDataFromNet(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = executeHttp { mService.getWxArticle() }
return flow { emit(response) }.flowOn(Dispatchers.IO)
}
suspend fun fetchDataFromDb(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = getDataFromRoom()
return flow { emit(response) }.flowOn(Dispatchers.IO)
}
Repository
中的返回不再直接返回實體類比庄,而是返回flow包裹的實體類對象。
為什么要這么做乏盐?
為了用神奇的flow操作符來處理佳窑。
flow組合操作符
combine、combineTransform
combine操作符可以連接兩個不同的Flow父能。merge
merge操作符用于將多個流合并神凑。zip
zip操作符會分別從兩個流中取值,當(dāng)一個流中的數(shù)據(jù)取完何吝,zip過程就完成了溉委。
關(guān)于 Flow 的基礎(chǔ)操作符,徐醫(yī)生大神的這篇文章已經(jīng)寫的很棒了爱榕,這里就不多余的寫了瓣喊。
根據(jù)操作符的示例可以看出,就算返回的不是同一個對象黔酥,也可以用操作符進行處理藻三。
幾年前剛開始學(xué)RxJava時洪橘,好幾次都是入門到放棄,操作符太多了棵帽,搞的也很懵逼熄求,F(xiàn)low 真的比它簡單太多了。
五逗概、flow的奇淫技巧
flowWithLifecycle
需求:
Activity 的 onResume()
方法中請求最新的地理位置信息弟晚。
以前的寫法:
// TestActivity.kt
override fun onResume() {
super.onResume()
getLastLocation()
}
override fun onDestory() {
super.onDestory()
// 釋放獲取定位的代碼,防止內(nèi)存泄露
}
這種寫法沒問題逾苫,也很正常指巡,但是用了 Flow 之后,有一種新的寫法隶垮。
用了 flow 的寫法:
// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getLastLocation()
}
@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation() {
if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
lifecycleScope.launch {
NetWorkLocationHelper(this)
.getNetLocationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}
}
}
}
在onCreate
中書寫該函數(shù),然后 flow 的鏈?zhǔn)秸{(diào)用中加入:
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
flowWithLifecycle
能監(jiān)聽 Activity 的生命周期秘噪,在 Activity 的onResume
開始請求位置信息狸吞,onStop
時自動停止,不會導(dǎo)致內(nèi)存泄露指煎。
flowWithLifecycle
會在生命周期進入和離開目標(biāo)狀態(tài)時發(fā)送項目和取消內(nèi)部的生產(chǎn)者蹋偏。
這個api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01
依賴庫。
callbackFlow
有沒有發(fā)現(xiàn)5.1中調(diào)用獲取位置信息的代碼很簡單至壤?
NetWorkLocationHelper(this)
.getNetLocationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}
幾行代碼解決獲取位置信息威始,并且任何地方都直接調(diào)用,不要寫一堆代碼像街。
這里就是用到callbackFlow
黎棠,簡而言之,callbackFlow
就是將callback
回調(diào)代碼變成同步的方式來寫镰绎。
這里直接上NetWorkLocationHelper
的代碼脓斩,具體細節(jié)自行 Google,因為這就不是網(wǎng)絡(luò)框架的內(nèi)容畴栖。
這里附上主要的代碼:
suspend fun getNetLocationFlow(context: Context): Flow<Location?> {
return callbackFlow<Location?> {
val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val consumer: java.util.function.Consumer<Location> = java.util.function.Consumer<Location> { location -> offer(location) }
locationManager.getCurrentLocation(LocationManager.NETWORK_PROVIDER, null, context.mainExecutor, consumer)
awaitClose()
} else {
val locationListener = LocationListener { location -> offer(location) }
locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, locationListener, Looper.getMainLooper())
awaitClose {
locationManager.removeUpdates(locationListener)
}
}
}
}
詳細代碼見Github
總結(jié)
上一篇文章# 兩種方式封裝Retrofit+協(xié)程随静,實現(xiàn)優(yōu)雅快速的網(wǎng)絡(luò)請求
加上這篇的 flow 網(wǎng)絡(luò)請求封裝,一共是三種對Retrofit+協(xié)程
的網(wǎng)絡(luò)封裝方式吗讶。
對比下三種封裝方式:
封裝一 (對應(yīng)分支oneWay) 傳遞ui引用燎猛,可按照項目進行深度ui定制,方便快速照皆,但是耦合高
封裝二 (對應(yīng)分支master) 耦合低重绷,依賴的東西很少,但是寫起來模板代碼偏多
封裝三 (對應(yīng)分支dev) 引入了新的flow流式編程(雖然出來很久膜毁,但是大部分人應(yīng)該還沒用到)论寨,鏈?zhǔn)秸{(diào)用星立,loading 和網(wǎng)絡(luò)請求以及結(jié)果處理都在一起,很多時候甚至都不要聲明 LiveData 對象葬凳。
第二種封裝我在公司的商業(yè)項目App中用了很長時間了绰垂,涉及幾十個接口,暫時沒遇到什么問題火焰。
第三種是我最近才折騰出來的劲装,在公司的新項目中(還沒上線)使用,也暫時沒遇到什么問題昌简。
如果某位大神看到這篇文章占业,有不同意見,或者發(fā)現(xiàn)封裝三有漏洞纯赎,歡迎指出谦疾,不甚感謝!
項目地址
項目持續(xù)更新...