Kotlin魔法——优雅实现多函数回调

写在前面

在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败的情况来进行处理,那么,在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层调用时习惯上都是可以提到最外层的,那么用第三种方式,如果还有其他需要注册的回调,就也可以都一并提到最外层啦,看起来就很高级和舒服呢!

就写到这里叭~

最后

学习完本文有没有收获到一点什么呢?学无止境,学习如逆水行舟,不进则退,本文除了以上内容,还准备了许多Android进阶练习的相关资料,为努力奋斗的你无偿献上,希望能帮到你!

扫码领取!Android开发必备进阶资料

Kotlin中,高阶函数可以用于实现回调。所谓高阶函数,就是可以接受一个或多个函数作为参数,或者返回一个函数函数。通过使用高阶函数,我们可以简化回调实现方式。 具体来说,我们可以将一个函数作为参数传递给另一个函数,然后在需要的时候调用传递进来的函数。这样可以让我们的代码更加简洁、灵活,并且减少了冗余的代码。 在使用Kotlin的高阶函数实现回调时,可以使用Lambda表达式来定义匿名函数,从而更加简化代码。Lambda表达式可以通过大括号 {} 来定义,可以在其中编写函数实现逻辑。 另外,为了方便在Java中调用Kotlin的高阶函数Kotlin提供了一系列Function接口类,最多支持22个参数。这些接口类可以用于定义函数类型,以便在Java中使用高阶函数时进行类型匹配。 总之,通过使用Kotlin的高阶函数,我们可以简化回调实现方式,使代码更加简洁、易读和灵活。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Kotlin 使用高阶函数实现回调](https://blog.csdn.net/u011494285/article/details/80546335)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Kotlin使用高阶函数实现多方法回调](https://blog.csdn.net/Heijinbaitu/article/details/100742964)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值