前段时间一直在忙鸿蒙相关的工作,最近忙里抽闲,总结一下之前在写 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 这种编码模式下,我们需要:
-
确定作用域内行为,对应抽象成类、接口;
// 入口 fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) //作用域内的行为声明 interface DslSpannableStringBuilder { //增加一段文字 fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null) } -
如果一个行为必须要被执行,我们可以设置一个特殊的
XxxResult类型,要求作用域函数以此类型作为返回值:fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> XxxResult)在作用域内声明一个必须被执行的函数:
interface DslSpannableStringBuilder { //增加一段文字 fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null) //必须执行的行为 fun mustCall():XxxResult } -
如果作用域是接口,要有对应的实现类;使用接口+实现类的方式可以隐藏内部;
理解了上面的这些知识之后,像这样“不好看的代码”,想必你也能理解为什么可以不调用onDispose了吧:
@Composable
fun useUnmount(block: () -> Unit) = DisposableEffect(Unit) {
object : DisposableEffectResult {
override fun dispose() {
block()
}
}
}