使用 Kotlin Flow 优化你的网络请求框架,减少模板代码

目的

本文不涉及 Flow 很深的东西,即使不会 Flow 也可以上手使用。

话接上篇文章 两种方式封装Retrofit+协程,实现优雅快速的网络请求

最近在独立写一个新的项目,用的是封装二,虽然几行代码就可以进行网络请求,但是在使用过程中还是觉得有点遗憾,写起来也不是非常快捷,存在模板代码。

加上很多小伙伴想要一个Flow版本的,忙里偷闲,用kotlin Flow对这套框架进行了优化,发现flow真香。

一、以前封装的遗憾点

主要集中在如下2点上:

  • Loading的处理

  • 多余的LiveData

总而言之,就是需要写很多模板代码。

不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小.

1.1 Loading的处理

对于封装二,虽然解耦比封装一更彻底,但是关于Loading这里我觉得还是有遗憾。

试想一下:如果Activity中业务很多、逻辑复杂,存在很多个网络请求,在需要网络请求的地方都要手动去showLoading() ,然后在 observer() 中手动调用 stopLoading()

假如Activity中代码业务复杂,存在多个api接口,这样Activity中就存在很多个与loading有关的方法。

此外,如果一个网络请求的showLoading()方法和dismissLoading()方法相隔很远。会导致一个顺序流程的割裂。

请求开始前showLoading() ---> 请求网络 ---> 结束后stopLoading(),这是一个完整的流程,代码也应该尽量在一起,一目了然,不应该割裂存在。

如果代码量一多,以后维护起来,万一不小心删除了某个showLoading()或者stopLoading(),也容易导致问题。

还有就是每次都要手动调用这两个方法,麻烦。

1.2 重复的LiveData声明

个人认为常用的网络请求分为两大类:

  • 用完即丢,只运行一次,返回一个结果

  • 需要监听数据变化,可以在一段时间内发出多个值

举个常见的例子,看下面这个页面:

image.png

用户一进入这个页面,绿色框里面内容基本不会变化,(不去纠结微信这个页面是不是webview之类的),这种ui其实是不需要设置一个LiveData去监听的,因为它几乎不会再更新了。

典型的还有:点击登录按钮,成功后就进去了下一个页面。

但是红色的框里面的ui不一样,需要实时刷新数据,也就用到LiveData监听,这种情况下观察者订阅者模式的好处才真正展示出来。并且从其他页面过来,LiveData也会把最新的数据自动更新。

对于用完即丢的网络请求,LoginViewModel会存在这种代码:

// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>()

并且对应的Activity中也需要监听这3个LiveData。

这种模板代码让我写的很烦。

用了Flow优化后,完美的解决这2个痛点。

“Talk is cheap. Show me the code.”

二、集成Flow之后的用法

2.1 请求自带Loading&&不需要监听数据变化

需求:

  • 不需要监听数据变化,对应上面的用完即丢

  • 不需要在ViewModel中声明LiveData成员对象

  • 发起请求之前自动showLoading(),请求结束后自动stopLoading()

  • 类似于点击登录按钮,finish 当前页面,跳转到下一个页面

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

需求:

  • 不需要监听数据变化

  • 不需要在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()

并且请求的结果直接在回调里面接收,直接处理,这样请求网络和结果的处理都在一起,看起来一目了然,再也不需要在 Activity 中到处找在哪监听的 LiveData

同样,它跟 LiveData 一样,也会监听 Activity 的生命周期,不会造成内存泄露。因为它是运行在ActivitylifecycleScope 协程作用域中的。

2.3 需要监听数据变化

需求:

  • 需要监听数据变化,要实时更新数据

  • 需要在 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()
    }
}

本质上是通过FLow来调用LiveDatasetValue()方法,还是LiveData的使用。虽然可以完全用 Flow 来实现,但是我觉得这里用 Flow 的方式麻烦,不容易懂,还是怎么简单怎么来。

这种方式其实跟上篇文章中的封装二差不多,区别就是不需要手动调用Loading有关的方法。

用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来实现顺序流和链式编程。

flow关键字大括号里面的是方法的执行,结果通过emit发送给下游。

onStart表示最开始调用方法之前执行的操作,这里是展示一个 loading ui

onCompletion表示所有执行完成,不管有没有异常都会执行这个回调。

collect表示执行成功的结果回调,就是emit()方法发送的内容,flow必须执行collect才能有结果。因为是冷流,对应的还有热流。

更多的Flow知识点可以参考其他博客和官方文档。

这里可以看出,通过Flow完美的解决了loading的显示与隐藏。

我这里是在Activity中都调用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()
    }
}

这样每次调用launchWithLoadingGetFlow方法,里面就实现了 Loading 的展示与隐藏,并且会返回一个 FLow 对象。

下一步就是处理 flow 结果collect里面的模板代码。

3.2 声明结果回调类

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 = {}
}

各种回调按照项目特性删减即可。

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()
}

上篇文章这里的处理用的是继承LiveDataObserver,这里就不需要了,毕竟继承能少用就少用。

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转换成LiveData对象

获取到的是Flow对象,如果想要变成LiveDataFlow原生就支持将Flow对象转换成不可变的LiveData对象。

val loginFlow: Flow<ApiResponse<User?>> =
    launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()

调用的是 Flow 的asLiveData()方法,原理也很简单,就是用了livedata的扩展函数:

@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的回调里面监听不同状态的callback

以前的方式是继承,有如下缺点:

  • 必须要用StateLiveData,不能用原生的LiveData,侵入性很强
  • 不只是继承LiveData,还要继承Observer,麻烦
  • 为了实现这个,写了一堆的代码

这里用 Kotlin 扩展实现,直接扩展 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 进一步完善

很多网络请求的相关并不是只有 loading 状态,还需要在请求前和结束后处理一些特定的逻辑。

这里的方式是:直接在封装方法的参数加 callback,默认用是 loading 的实现。

fun <T> BaseActivity.launchAndCollect(
    requestBlock: suspend () -> ApiResponse<T>,
    startCallback: () -> Unit = { showLoading() },
    completeCallback: () -> Unit = { dismissLoading() },
    listenerBuilder: ResultBuilder<T>.() -> Unit
)

四、针对多数据来源

虽然项目中大部分都是单一数据来源,但是也偶尔会出现多数据来源,多数据源结合Flow的操作符,也非常的方便。

示例

假如同一份数据可以从数据库获取,可以从网络请求获取,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操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。

关于 Flow 的基础操作符,徐医生大神的这篇文章已经写的很棒了,这里就不多余的写了。

根据操作符的示例可以看出,就算返回的不是同一个对象,也可以用操作符进行处理。

几年前刚开始学RxJava时,好几次都是入门到放弃,操作符太多了,搞的也很懵逼,Flow 真的比它简单太多了。

五、flow的奇淫技巧

flowWithLifecycle

需求:
Activity 的 onResume() 方法中请求最新的地理位置信息。

以前的写法:

// TestActivity.kt
override fun onResume() {
    super.onResume()
    getLastLocation()
}

override fun onDestory() {
    super.onDestory()
    // 释放获取定位的代码,防止内存泄露
}

这种写法没问题,也很正常,但是用了 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中书写该函数,然后 flow 的链式调用中加入:

.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)

flowWithLifecycle能监听 Activity 的生命周期,在 Activity 的onResume开始请求位置信息,onStop 时自动停止,不会导致内存泄露。

flowWithLifecycle 会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。

这个api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01依赖库。

callbackFlow

有没有发现5.1中调用获取位置信息的代码很简单?

NetWorkLocationHelper(this)
    .getNetLocationFlow()
    .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
    .collect { location -> 
         Log.i(TAG, "最新的位置是:$location")
    }

几行代码解决获取位置信息,并且任何地方都直接调用,不要写一堆代码。

这里就是用到callbackFlow,简而言之,callbackFlow就是将callback回调代码变成同步的方式来写。

这里直接上NetWorkLocationHelper的代码,具体细节自行 Google,因为这就不是网络框架的内容。

这里附上主要的代码:

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

总结

上一篇文章# 两种方式封装Retrofit+协程,实现优雅快速的网络请求

加上这篇的 flow 网络请求封装,一共是三种对Retrofit+协程的网络封装方式。

对比下三种封装方式:

  • 封装一 (对应分支oneWay) 传递ui引用,可按照项目进行深度ui定制,方便快速,但是耦合高

  • 封装二 (对应分支master) 耦合低,依赖的东西很少,但是写起来模板代码偏多

  • 封装三 (对应分支dev) 引入了新的flow流式编程(虽然出来很久,但是大部分人应该还没用到),链式调用,loading 和网络请求以及结果处理都在一起,很多时候甚至都不要声明 LiveData 对象。

第二种封装我在公司的商业项目App中用了很长时间了,涉及几十个接口,暂时没遇到什么问题。

第三种是我最近才折腾出来的,在公司的新项目中(还没上线)使用,也暂时没遇到什么问题。

如果某位大神看到这篇文章,有不同意见,或者发现封装三有漏洞,欢迎指出,不甚感谢!

项目地址

FastJetpack

项目持续更新...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容