Kotlin 函数作用域 - 深入理解 Compose 中的 DisposeEffect 副作用函数

519 阅读4分钟

前段时间一直在忙鸿蒙相关的工作,最近忙里抽闲,总结一下之前在写 ComposeHooks 项目的一些小小心得。

DSL,其实在这里更多指的是利用作用域概念,限定函数闭包内的函数调用行为(下面称之为作用域内行为)。

在之前的文章:# 在 Kotlin 中巧妙的使用 DSL 封装 SpannableStringBuilder

提到了编写 DSL 的一些小心得,总结如下:

期望一个闭包得内部行为,编写相应的作用域接口:

interface 作用域名称接口 {
    //作用域内行为:增加一段文字
    fun addText(text: String)
}

这里得作用域名称接口建议通过 XxxScope 这种格式命名。


我们回到 Compose 源码 - DisposeEffect 中继续学一下,看看Compose团队是如何使用这种编码技巧的:

@Composable
@NonRestartableComposable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
) {
    remember(key1) { DisposableEffectImpl(effect) }
}

这里关注两个类型:DisposableEffectScope 是这里的作用域DisposableEffectResult是副作用effect函数的返回值,他们的源码如下:

class DisposableEffectScope {
    /**
     * Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition
     * or its key changes.
     */
    inline fun onDispose(
        // 传入 onDispose 的闭包
        crossinline onDisposeEffect: () -> Unit
    ): DisposableEffectResult = object : DisposableEffectResult {
        override fun dispose() {
            // 真正调用传入的 onDispose 闭包
            onDisposeEffect()
        }
    }
}


interface DisposableEffectResult {
    fun dispose()
}

DisposableEffectScope 作用域不是一个接口而是一个具体的类,其中只有一个内联函数 onDispose,也就是说在我们的 DisposableEffect 闭包中仅可以调用这一个作用域内行为

同时由于effect副作用函数 effect: DisposableEffectScope.() -> DisposableEffectResult 要求必须返回 DisposableEffectResult,这就实际上规范了我们必须要在最后一行调用这个内联函数 onDispose(因为其返回值也正是我们副作用函数限定的返回值类型DisposableEffectResult)。

DisposableEffectResult 实际上是对 onDispose函数接收的闭包的一个包装类型!

我们传入的闭包函数是通过DisposableEffectImpl 这个类实现的相关重组逻辑,他其实是RememberObserver的一个实现类。

//compose内部私有的作用域实例
private val InternalDisposableEffectScope = DisposableEffectScope()
private class DisposableEffectImpl(
    // 副作用effect函数被传递到了实现类中
    private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {
    private var onDispose: DisposableEffectResult? = null

    override fun onRemembered() {
        // remember 被记住时,此时创建作用域实例,执行作用域函数
        onDispose = InternalDisposableEffectScope.effect()
    }

    override fun onForgotten() {
        onDispose?.dispose()
        onDispose = null
    }

    override fun onAbandoned() {
        // Nothing to do as [onRemembered] was not called.
    }
}

第一步:在 onRemembered 执行时我们传入的闭包effect会被执行(此时他的作用域,或者说函数的接收者是InternalDisposableEffectScope),还记得这个副作用effect函数的签名是什么吗?

答案: effect: DisposableEffectScope.() -> DisposableEffectResult

也就说我们闭包内的代码会执行,通过 onDispose 函数创建的DisposableEffectResult实例缓存到:var onDispose

这里实际缓存的也就是 onDispose 函数接收的卸载时执行的闭包函数。

第二步:当我们组件卸载时,记住的内容被忘记,onForgotten 回调执行。此时会调用var onDispose 中的 dispose 函数。

还记得这个函数会做什么吗?

inline fun onDispose(
    // 传入 onDispose 的闭包
    crossinline onDisposeEffect: () -> Unit
): DisposableEffectResult = object : DisposableEffectResult { //包装传入的闭包
    override fun dispose() {
        // 真正调用传入的 onDispose 闭包
        onDisposeEffect()
    }
}

也就是实际在执行我们写在 onDispose 函数中的卸载执行的闭包


再次加深记忆:

在 DSL 这种编码模式下,我们需要:

  1. 确定作用域内行为,对应抽象成类、接口;

    // 入口
    fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) 
    //作用域内的行为声明
    interface DslSpannableStringBuilder {
        //增加一段文字
        fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null)
    }
    
  2. 如果一个行为必须要被执行,我们可以设置一个特殊的XxxResult类型,要求作用域函数以此类型作为返回值:

    fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> XxxResult)
    

    在作用域内声明一个必须被执行的函数:

    interface DslSpannableStringBuilder {
        //增加一段文字
        fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null)
        //必须执行的行为
        fun mustCall():XxxResult
    }
    
  3. 如果作用域是接口,要有对应的实现类;使用接口+实现类的方式可以隐藏内部;

理解了上面的这些知识之后,像这样“不好看的代码”,想必你也能理解为什么可以不调用onDispose了吧:


@Composable
fun useUnmount(block: () -> Unit) = DisposableEffect(Unit) {
    object : DisposableEffectResult {
        override fun dispose() {
            block()
        }
    }
}