利用Kotlin和协程实现DSL样式的网络请求

利用Kotlin和协程实现DSL样式的网络请求

本文将基于retrofit2.62、okhttp4.0、Coroutines、viewModel-ktx、LiveData-ktx力求实现一种分层清晰、整洁灵活、处理方便的网络请求。

技术栈

为了拥抱Kotlin,okHttp已经将okhttp全部用Kotlin重写。同时okHttp的老朋友retrofit也拥抱了Coroutines推出了retrofit2.60。

DSL方式的语法特性or代码样式在各个开源库中也露脸越来越多。比如刚刚官宣停止维护的Anko和如日中天的Flutter。关于DSL的更多介绍本文最后将给出学习链接。DSL的书写风格在灵活配置请求和处理请求上给人耳目一新、整洁灵活、清晰可读的观感。

本文所实现网络请求的特点

DSL方式的请求,自由处理各种start、response、error回调,或者交给BaseViewModel统一处理

回调方式请求,自由处理各种start、response、error回调,或者交给BaseViewModel统一处理

LiveData方式请求,请求直接返回LiveData

DSL方式灵活配置OkHttpClient/Retrofit

ShowCode

请求的声明如下

interface TestService {
    @GET("/banner/json")
    suspend fun getBanner(): WanResponse<List<Banner>>
}

可以看到加成了协程以后的retrofit在生命网络请求以后变得异常简单,不需要用Call或者Observable进行包装,直接返回想要的实体类就好。suspend是Kotlin的关键字,修饰方法是表示为挂起函数,只能运行在协程或者其他挂起函数中。

请求的OKHttp、Retrofit的配置示例如下

 Request.init(context = this.applicationContext, baseUrl = "https://www.wanandroid.com") {
            okHttp {okhttpBuilder->
                //配置okhttp
                okhttpBuilder
            }

            retrofit {retrofitBuilder->
                //配置retrofit
                retrofitBuilder
            }
        }

如示例代码所示,通过DSL化的代码书写方式可以灵活的通过okHttp或者retrofit代码块来灵活配置okHttp和retrofit。当然用户可以直接选择不进行任何配置,基本的配置在Request.kt中已经配置完成,在使用默认配置的情况下完全可以不书写okHttp或者retrofit代码块。需要说明的是初始化过程中传入了context,是由于在Request.kt中存在关于持久化cookie的配置,cookie持久化到SP中时需要context来创建SP。还有okHttp和retrofit看似是代码块其实是带函数类型参数的方法而已,正是利用了kotlin对高阶函数、扩展函数、lambda表达式的友好支持和invoke约定才能写出如上所示的DSL化的保证可读性的整洁灵活的代码。

关于DSL方式请求调用示例如下

class TestViewModel : BaseViewModel() {
    private val service by lazy { Request.apiService(TestService::class.java) }
    val liveData = MutableLiveData<WanResponse<List<Banner>>>()
    fun loadDSL() {
        apiDSL<WanResponse<List<Banner>>> {
            onRequest {
                service.getBanner()
            }
            onResponse {response->
                Log.e("Thread-->onResponse", Thread.currentThread().name)
                Log.e("onResponse-->", Gson().toJson(response))
                liveData.value = response
            }
            onStart {
                Log.e("Thread-->onStart", Thread.currentThread().name)
                false
            }
            onError {
                it.printStackTrace()
                Log.e("Thread-->onError", Thread.currentThread().name)
                true
            }
        }
    }
}

如上可见,在onRequest中一股脑塞入请求就可以在onResponse中拿到请求结果。同时也可以在主线程的onStart中自由预处理一些逻辑,可以看到onStart代码块最后默认返回了false,false表示不拦截BaseViewModel中对网络请求开始时的处理(比如弹出统一样式的loading)。如果返回true则表示该行为完全由自己处理。同理针对onError也是一样的道理,可以自己处理错误也可以交给base处理。当然也可以不写onStart和onError完全交给base来处理相关行为,使网络请求代码更简洁。

关于回调方式请求调用示例如下

fun loadCallback() {
    apiCallback({
        service.getBanner()
    }, {
        liveData.value = it//这里是onResponse的回调
    }, {
        true//这里是onStart的回调
    },  onError ={ exception ->
        false
    })
}

借助函数类型(Any) -> Any来定义请求的不同回调,比如error的回调可以定义为((Exception) -> Boolean)?。接受exception来处理异常,返回bool类型来决定是否继续交给base来继续处理。同时定义成可空类型可以默认交给base出路。但是显而易见的是这种代码书写方式并不如DSL方式的请求美观和可读性高。

关于直接返回LiveData的请求调用示例如下

fun loadLiveData(): LiveData<Result<WanResponse<List<Banner>>>> {
        return apiLiveData(SupervisorJob() + Dispatchers.Main.immediate, timeoutInMs = 2000) {
            service.getBanner()
        }
 }

在V层拿到LiveData后的操作如下

viewModel.loadLiveData().observe(this, Observer {
                when (it) {
                    is Result.Error -> {
                        hideLoading()
                    }
                    is Result.Response -> {
                        hideLoading()
                        it.response.apply {
                            showToast(Gson().toJson(this))
                        }
                    }
                    is Result.Start -> {
                        showLoading()
                    }
                    else ->{//冗余
                    }
                }
})

显然这种方式的请求更适合轻量化的请求,适合拿到结果直接去渲染view不经过二次数据处理的场景。因为如上图所示在V层处理start、error回调感觉不是很友好,,在reponse中隐藏loading也是比较繁琐。但好处是V层直接可以拿到包含请求数据的LiveData,操作更加便捷。

关于Livedata的封装如下

protected fun <Response> apiLiveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = 3000L,
    request: suspend () -> Response
    ): LiveData<Result<Response>> {

    return androidx.lifecycle.liveData(context, timeoutInMs) {
        emit(Result.Start())
        try {
            emit(withContext(Dispatchers.IO) {
                Result.Response(request())
            })
        } catch (e: Exception) {
            e.printStackTrace()
            emit(Result.Error(e))
        } finally {
            emit(Result.Finally())
        }
    }
}

此处的livedata是lifecycle-livedata-ktx,在配置了timeoutInMs后如果没有活跃的observers就会超时自动取消。在IO线程拿到请求的结果后包装成Result,像RxJava那样发射出来即可。为了保证返回的livedata中数据的一致性,start、error也被包装成了Result。

DSL封装示例

接下来我们以对okhttp和retrofit的请求配置来看下是怎么进行DSL封装的,不多说showcode。

class RequestDsl {
    internal var buidOkHttp: ((OkHttpClient.Builder) -> OkHttpClient.Builder)? = null
    internal var buidRetrofit: ((Retrofit.Builder) -> Retrofit.Builder)? = null
    fun okHttp(builder: ((OkHttpClient.Builder) -> OkHttpClient.Builder)?) {
        this.buidOkHttp = builder
    }
    fun retrofit(builder: ((Retrofit.Builder) -> Retrofit.Builder)?) {
        this.buidRetrofit = builder
    }
}

首先是DSL的配置类,主要有2个角色,一个是函数类型的buidOkHttp,一个是以buidOkHttp为参数的配置buidOkHttp的高阶函数okHttp。可见buidOkHttp变量是一个可空类型的输入和返回是非空的OkHttpClient.Builder类型的函数,既然是可空类型的我们在初始化调用时就可以选择配置OkHttpClient.Builder与否。既然输入返回都是OkHttpClient.Builder我们就可以拿到既定的带有初始化配置的OkHttpClient.Builder进行进一部配置,只要最后返回OkHttpClient.Builder就好,同时OkHttpClient.Builder采用了建造者模式我们可以拿到builder引用之后进行二次配置最后原样返回builder的引用。

下面是初始化方法的具体实现

private fun initRequest(okHttpBuilder: OkHttpClient.Builder, requestDSL: (RequestDsl.() -> Unit)? = null) {
    val dsl = if (requestDSL != null) RequestDsl().apply(requestDSL) else null
    val finalOkHttpBuilder = dsl?.buidOkHttp?.invoke(okHttpBuilder) ?: okHttpBuilder
    val retrofitBuilder = Retrofit.Builder()
        .baseUrl(this.baseUrl)
        .addConverterFactory(GsonConverterFactory.create())
        .client(finalOkHttpBuilder.build())
    val finalRetrofitBuilder = dsl?.buidRetrofit?.invoke(retrofitBuilder) ?: retrofitBuilder
    this.retrofit = finalRetrofitBuilder.build()
}

这个方法就比较简单,requestDSL定义为可空类型,可以选择配置或者不进行额外配置。

此时我们再看一下比较常用的apply方法的如下定义,我们在apply方法中就进入到了泛型T的内部空间,this关键字就指代的是泛型自己 ,可以在内部调用泛型的成员。

public inline fun <T> T.apply(block: T.() -> Unit): T

相似的requestDSL也是和apply方法中的block是一样的类型。一旦选择了进行配置就可以像apply方法一样,在RequestDsl函数内部选择性的调用okHttp或者retrofit方法。那么在关于DSL方式请求调用也和配置请求一样如出一辙不再多说。

协程的使用

internal fun launch(viewModelScope: CoroutineScope) {
    viewModelScope.launch(context = Dispatchers.Main) {
        onStart?.invoke()
        try {
            val response = withContext(Dispatchers.IO) {
                request()
            }
            onResponse?.invoke(response)
        } catch (e: Exception) {
            e.printStackTrace()
            onError?.invoke(e)
        } finally {
            onFinally?.invoke()
        }
    }
}

整个项目中关于协程的使用就只有这一个方法,其中viewModelScope可以是在viewmodel-ktx中定义的协程作用域,来避免我们书写重复的代码。同在ViewModel.onCleared()被调用的时候,viewModelScope会自动取消作用域内的所有协程。在执行请求任务request()时会切换到IO线程执行,拿到结果后通过onResponse告诉上层代码。

最后关于base中的统一处理回调的示例

如下代码都是在BaseViewModel中定义的

protected fun <Response> apiDSL(apiDSL: ViewModelDsl<Response>.() -> Unit) {
    api<Response> {
        onRequest {
            ViewModelDsl<Response>().apply(apiDSL).request()
        }
        onResponse {
            ViewModelDsl<Response>().apply(apiDSL).onResponse?.invoke(it)
        }
        onStart {
            val override = ViewModelDsl<Response>().apply(apiDSL).onStart?.invoke()
            if (override == null || !override) {
                onApiStart()
            }
            override
        }
        onError { error ->
            val override = ViewModelDsl<Response>().apply(apiDSL).onError?.invoke(error)
            if (override == null || !override) {
                onApiError(error)
            }
            override
        }
    }
}

我们重点关注api请求发起时start、出错时error的处理,其中涉及的onApiStart()和onApiFinally()的定义如下

protected open fun onApiStart() {
    apiLoading.value = true//apiLoading: MutableLiveData<Boolean>
}
protected open fun onApiError(e: Exception?) {
    apiLoading.value = false
    apiException.value = e//apiException: MutableLiveData<Throwable>
}

在方法apiDSL中进行了一次DSL的嵌套,apiDSL是业务代码配置的代码。如果apiDSL没有配置onStart或者最后返回了false,那么表示还需要base进一步处理start的回调.此时就会调用base中定义的onApiStart()更新loading的liveData,然后再V层中拿到apiLoading统一弹出或关闭loadingDialog。

代码地址

https://github.com/RunFeifei/Run

感谢

像使用gradle一样,在kotlin中进行网络请求

首先感谢该作者,正是由于这篇文章我自己才有了从头到尾动手走一遍的想法,才有了本文,感谢!!

Kotlin DSL原理解析:带接收者的lambda以及invoke约定

感谢该作者,这篇文档真正让我开始渐渐熟悉DSL,慢慢理会DSL

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