补充

写完这篇文章并发布了之后才发现,这个写法现已有人发布过了,也能够参阅参阅~

怎样让你的回调更具Kotlin风味

Kotlin DSL回调

写在前面

在网络恳求时,常常面临一类状况:网络恳求有或许成功,也有或许失利,这就需求两个回调函数来别离对成功和失利的状况来进行处理,那么,在Kotlin这门无比强大的语言中,有没有一种“魔法”,能够高雅地完结这一类一起或许需求多个回调的场景呢?

场景

问题的场景现已提出,也便是当某一个行为需求有多个回调函数的时分,而且这些回调并不一定都会触发。

例如,网络恳求的回调场景中,有时分是onSuccess触发,有时分是onFailure触发,这两个函数的函数签名也不一定相同,那么怎样完结这个需求呢?

接下来咱们以一个详细的问题贯穿全文:

假定咱们现在要写一个网络恳求结构,在封装上层回调的时分,需求封装两个回调(onSuccess/onFailure)供上层(就假定是UI层吧,不搞什么MVVM架构了)调用,以便UI层能知道网络恳求成功/失利了,并进行相应的UI更新。

注: 标题所说的“魔法”是指完结办法三,办法一和二只是为了三铺垫的引子,假如想直奔主题那么主张直接跳转完结办法三!

完结办法一:直接传参

最直接的当然是直接传参嘛,把这两个回调写成函数参数,直接传进去,这当然能够完结目标,简单的示例代码如下。

网络恳求层

data class RequestConfig(val api: String, val bodyJson: String, val method: String = "POST")
data class Data(val myData1: Int, val myData2: Boolean)
//模仿网络恳求,获取数据
fun fetchData(requestConfig: RequestConfig, onSuccess: (data: Data) -> Unit = {}, onFailure: (errorMsg: String) -> Unit = {}) {
    //假定调用更底层如Retrofit等模块,成功拿到数据后调用
    onSuccess(Data(1, true))
    //或许,失利后调用
    onFailure("断网啦")
}

UI层

@Composable
fun MyView() {
    Button(onClick = { 
        fetchData(requestConfig = RequestConfig("/user/info", ""), onSuccess = {
            //更新UI
        }, onFailure = {
            //弹Toast提示用户
        })
    }) { }
}

在网络恳求层,经过把fetchData的回调参数设一个默许值,咱们也能完结“回调可选”这一需求。

这好像并没有什么问题,那么还有没有什么别的完结办法呢?

完结办法二:链式调用

简单的考虑过后,发现链式调用好像也能满足咱们的需求,完结如下。

网络恳求层

在网络恳求层,咱们预先封装一个表明恳求成果的类MyResult,然后让fetchData返回这个成果。

data class MyResult(val code: Int, val msg: String, val data: Data) {
    fun onSuccess(block: (data: Data) -> Unit) = this.also {
        if (code == 200) { //判别交给MyResult,若code==200,则认为成功
            block(data)
        }
    }
    fun onFailure(block: (errorMsg: String) -> Unit) = this.also {
        if (code != 200) { //判别交给MyResult,若code!=200,则认为失利
            block(msg)
        }
    }
}
//模仿网络恳求,获取数据
fun fetchData(requestConfig: RequestConfig): MyResult {
    return retrofitRequest(requestConfig)
}

UI层

此时的UI层调用fetchData时,则是经过MyResult这个返回值进行链式调用,而且链式调用也是自在可选的。

@Composable
fun MyView() {
    Button(onClick = {
        //点击按钮后发送网络恳求
        fetchData(requestConfig = RequestConfig("/user/info", "")).onSuccess {
            //更新UI
        }.onFailure {
            //弹Toast提示用户
        }
    }) { }
}

这也好像并没有什么问题,可是,总感觉不够Kotlin!

其实写多了Kotlin就会发现,Kotlin好像非常喜欢花括号{},也便是效果域或许lambda这个概念。

而且Kotlin还喜欢把最终一个花括号放在最终一个参数,以便提到最外层去。

那么!有没有一种办法,能够以Kotlin常见的效果域的办法,高雅地完结上述场景需求呢?

锵锵!主角上台!

完结办法三:承继+扩展函数=魔法!

不多说,让咱们先来看看这种完结办法的效果!

用这种办法,上述UI层将会变成这样!

  • 假如什么也不需求处理
@Composable
fun MyView2() {
    Button(onClick = {
        //点击按钮后发送网络恳求
        fetchData(requestConfig = RequestConfig("/user/info", ""))
    }) { }
}
  • 假如需求处理onSuccess
@Composable
fun MyView2() {
    Button(onClick = {
        //点击按钮后发送网络恳求
        fetchData(requestConfig = RequestConfig("/user/info", "")) {
            onSuccess {
                //更新UI
            }
        }
    }) {
    }
}
  • 假如需求一起能处理onSuccess和onFailure
@Composable
fun MyView2() {
    Button(onClick = {
        //点击按钮后发送网络恳求
        fetchData(requestConfig = RequestConfig("/user/info", "")) {
            onSuccess {
                //更新UI
            }
            onFailure {
                //弹Toast提示用户
            }
        }
    }) {
    }
}

看到了吗!!!非常自在,而且没有任何剩余的->.或许,,只有非常整齐的花括号!

真的太神奇啦!

那么,这是怎样做到的呢?

揭秘时刻

在网络恳求层,咱们需求先界说一个接口,用于界说咱们需求的多个回调函数!

interface ResultScope {
    fun onSuccess(block: (data: Data) -> Unit)
    fun onFailure(block: (errorMsg: String) -> Unit)
}

接着咱们自己在内部完结这个接口!

internal class ResultScopeImpl : ResultScope {
    var onSuccessBlock: (data: Data) -> Unit = {}
    var onFailureBlock: (errorMsg: String) -> Unit = {}
    override fun onSuccess(block: (data: Data) -> Unit) {
        onSuccessBlock = block
    }
    override fun onFailure(block: (errorMsg: String) -> Unit) {
        onFailureBlock = block
    }
}

能够看到,咱们在完结类里界说了两个block成员变量,它正对应着咱们接口中的参数block,在重写接口办法时,咱们给这两个成员变量赋值。

其实便是把这个block先暂时记录下来啦。

最终便是咱们的fetchData函数了。

//模仿网络恳求,获取数据
fun fetchData(requestConfig: RequestConfig, resultScope: ResultScope.() -> Unit = {}) {
    val result = retrofitRequest(requestConfig)
    val resultScopeImpl = ResultScopeImpl().apply(resultScope)
    resultScopeImpl.run {
        if (result.code == 200) onSuccessBlock(result.data) else onFailureBlock(result.msg)
    }
}

fetchData的第一个参数自然是requestConfig,而最终一个参数则是一个带ResultScope类型接收器的代码块,咱们也给一个默许的空完结,以应对不需求任何onSuccess或许onFailure的状况。


那么首先就有第一个问题了!resultScope: ResultScope.() -> Unit这个参数怎样了解?

咱们首先要了解什么是lambda,或许说了解什么是接口!

重要!精髓! 怎样了解lambda的含义?

当面对一堆lambda,乃至是嵌套lambda的时分,你是否感觉到阅读困难,非常无力?假如是的话,其实有一个很简单的办法,lambda也便是一个函数表达式嘛~既然是函数,那么咱们就只需求盯紧三件事!

  • 函数的签名(包含参数列表和返回值)
  • 函数的办法体(也便是函数的完结)
  • 谁来担任在什么时分调用这个函数

只需盯紧这三件事,那么lambda的绝大部分了解上的障碍,都会一扫而光

例如

咱们常常所说的回调,比方这个网络恳求回调,那不便是:

  • 网络恳求结构担任约好函数的签名,其间
    • 参数列表代表待会儿我结构层拿到成果今后需求奉告你UI层哪些信息
    • 返回值代表你UI层在知道我结构给的信息,并处理完之后,需求再返回给我结构层什么成果
  • UI层担任这个lambda的详细完结,也便是
    • 怎样去处理刚刚从结构层传来的信息(即参数)
    • 奉告结构层处理完毕后的成果(即返回值)
  • 最终,上面通通都约好好之后,这时分的函数是一个死的函数,它只是界说好了,可是并没有去运转、没有被调用,那么,咱们最终需求弄清的,便是谁来担任在什么时分调用这个函数
  • 无疑是结构层来调用,结构层在从更下层获取到恳求成果后,就会调用这个函数,而且按之前所约好、所界说好的一切去履行它

又例如

Android开发中,RecyclerView这一列表组件会使用适配器,其间abstract void onBindViewHolder(@NonNull VH holder, int position)这个办法就也能够看成是一个所谓的lambda

  • 这个办法的签名和返回值由抽象类Adapter所界说
  • 这个办法的完结由Adapter的子类完结,即咱们自己写的适配器
  • 这个办法的调用由RecyclerView控件担任调用

也便是说,当列表滑动,需求加载第position项去显现时,RecyclerView的内部逻辑将会调用这个onBindViewHolder函数来向咱们索要第position项的视图,也便是有一个ViewHolder和一个position参数会被RecyclerView传给咱们,咱们需求在这个ViewHolder里正确放置第position项的内容,这便是适配器的工作原理

小结

那么,现在对lambda的了解,应该不成问题了吧,其实了解之后,lambda、abstract函数、接口、函数类型的参数、typeAlias…等等都是一个意思,咱们需求关注的是,它的界说、完结以及调用者和调用时机

回到正题,怎样了解resultScope: ResultScope.() -> Unit呢?

ResultScope.() -> Unit 表明一个带ResultScope类型接收器的函数代码块,说浅显一点,便是:

  • 在UI层调用fetchData的时分,它所传的那个参数resultScope,自身的效果域现已带有this了,这个this便是ResultScope类型的对象
    • 再说浅显一点便是,resultScope那个代码块内,能直接访问ResultScope的办法或许特点,这也便是为什么在上面的示例代码里,咱们能直接在花括号里写 onSuccess {} 的原因,由于那个花括号现已被ResultScope对象统治了,咱们能在里边直接调用ResultScope类的办法onSuccess
  • 然后,在网络恳求层,当恳求有成果后,咱们会调用ResultScope的实例的对应block办法
    • 由于调用者是ResultScope的实例,那么自然而然地,resultScope这个代码块就有了隐式this,换句话说,resultScope这个参数的类型能够看成(scope: ResultScope) -> Unit,只不过,在其详细完结代码块内部看不见scope这个参数,由于其自身现已是this的概念了,所以在UI层,咱们看到的onSuccess{}实际上是this.onSuccess{}

好,下一个问题。

在刚刚怎样了解resultScope参数的解读里,有一句粗体“咱们会调用ResultScope的实例的对应block办法”,那么,下一个问题便是,ResultScope的实例是怎样来的

ResultScope是一个接口,所以想要实例,咱们首先得给它整一个完结类,也便是ResultScopeImpl类,这个类直接完结了ResultScope,一起,界说了两个代码块成员变量,它正对应着咱们接口中的参数代码块,也便是成功或失利后,需求UI层做出处理的代码块onSuccess/onFailure,在重写接口办法时,咱们给这两个成员变量赋值。

那么最终的问题便是 怎样让这个ResultScopeImpl实例持有咱们UI层中界说的block(即onSuccess/onFailure) 了。

方才咱们不是在重写的办法中,将UI层界说的block赋值给了ResultScopeImpl中的成员变量onSuccessBlock/onFailureBlock了吗?

那咱们只需触发赋值,也便是ResultScopeImpl中override fun onSuccess的调用就行了。

办法便是这个!ResultScopeImpl().apply(resultScope)

咱们先new出一个ResultScopeImpl实例,然后resultScope不是正好包含了UI层界说的onSuccess/onFailure函数体吗?那咱们apply使用/赋值/设置特点)一下就能够了呗~

什么?你不知道为什么apply一下就能赋值了

一开始,new出了一个ResultScopeImpl实例,这时它的成员变量onSuccessBlock/onFailureBlock是咱们设置的默许值{},然后咱们让它进行apply,来看看apply这个效果域函数的源码~

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

发现了吗?apply的参数正好便是T.() -> Unit类型,这儿的T不便是ResultScopeImpl吗?那也便是说,block这个代码块会有一个隐式的this对象,这个this便是咱们刚刚创建的ResultScopeImpl实例,它来作为隐式this履行这个代码块,那么block代码块里边是什么呢?对啦,便是咱们在UI层写的onSuccess和onFailure嘛!由于ResultScopeImpl重写了接口的onSuccess/onFailure,因而履行的便是重写后的办法,这时分,ResultScopeImpl的成员变量block不就被赋上值了吗!over!

那么,完好的流程便是~

  • UI层的Button触发onClick,进而触发fetchData调用
  • fetchData内部创建了一个ResultScopeImpl实例,而且将UI层界说的onSuccess和onFailure这两个代码块拿了过来,作为ResultScopeImpl实例自己的成员变量onSuccessBlock/onFailureBlock
  • fetchData得到成果后,调用它自己的成员变量onSuccessBlock/onFailureBlock,实际上也便是调用了onSuccess和onFailure
  • UI层得到呼应,onSuccess/onFailure被调用,触发UI更新

结语

完结办法就介绍到这儿啦,当然,第三种办法并不是没有缺点,假如说,需求多次完结onSuccess回调,那么第三种办法,以上面的代码就不便利做到啦,只能把override里改成add,然后成员变量block们用一个List存起来,然后依次触发~

而假如是链式调用的完结办法,就不会有这个问题啦!

别的的话,假如你是一名Jetpack Compose开发者,例如Compose中能够带有子视图的组件(即相似ViewGroup的),最终都会有一个@Composable的代码块参数,UI层调用时习惯上都是能够提到最外层的,那么用第三种办法,假如还有其他需求注册的回调,就也能够都一并提到最外层啦,看起来就很高级和舒畅呢!

就写到这儿叭~