美文网首页kotlinKotlinAndroid常用框架结构
使用 Kotlin Flow 优化你的网络请求框架,减少模板代码

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

作者: ldlywt | 来源:发表于2021-12-05 11:20 被阅读0次

    目的

    本文不涉及 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

    项目持续更新...

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/iwuixrtx.html