我将尝试用一个简短的例子来描述问题。
假设我们有一个代表自定义开关的子可组合项,并且我们希望保持它不可变,因此我们需要传递初始状态和 lambda,以在用户切换开关时更改其真实来源:
@Composable
fun CustomSwitch(
title: String? = null,
checked: Boolean = false,
onSwitchChanged: ((Boolean) -> Unit)? = null
){
//...
}
我们现在有一个父可组合项,代表屏幕上有许多开关的部分。如果我们也想保持它不可变,我们需要通过参数向上公开子级的所有属性:
@Composable
fun PreferencesCard(
switch1Title: String? = null,
switch1Checked: Boolean = false,
OnSwitch1Changed: ((Boolean) -> Unit)? = null,
switch2Title: String? = null,
switch2Checked: Boolean = false,
OnSwitch2Changed: ((Boolean) -> Unit)? = null,
){
CustomSwitch(switch1Title, switch1Checked, OnSwitch1Changed)
CustomSwitch(switch2Title, switch2Checked, OnSwitch2Changed)
//Other composables
}
免责声明:在这个例子中,这个
PreferencesCard
可组合项真的很愚蠢,因为它什么也不做,它可以被 Column
或某种仅采用函数体的“开放”可组合项替换。但这只是因为我想让代码保持简单,请假设它有其他子级并且也做自己的事情。
使用这种方法,当我们在可组合项的层次结构中向上时,我们需要携带所有子参数,从而导致父参数列表非常长。这个问题是声明式 UI 框架的特征,例如在 React 中,它被称为“属性钻取”。它会产生复杂且难以维护的代码,因为子级中的任何更改(例如:添加新参数)都会导致其所有父级发生更改。它违背了封装的概念。 如果我们仍想保持父可组合项不可变,一种解决方案是封装子状态和侦听器,以便参数列表更短:
data class PreferencesCardState(val switch1Title: String?, val switch1Checked: Boolean, val switch2Title: String?, val switch2Checked: Boolean)
interface PreferencesCardListener {
fun onSwitch1Changed(b: Boolean): Unit
fun onSwitch2Changed(b: Boolean): Unit
}
@Composable
fun PreferencesCard(
state: PreferencesCardState,
listener: PreferencesCardListener
){
CustomSwitch(state.switch1Title, state.switch1Checked, {value -> listener?.onSwitch1Changed(value)})
CustomSwitch(state.switch2Title, state.switch2Checked, {value -> listener?.onSwitch2Changed(value)})
//Other composables
}
从状态的角度来看,这很好,因为 Compose 足够智能,仅当子参数更改时才更改子级(具有不可变属性的数据类是稳定的)。但是使用 lambda,我们可能会遇到臭名昭著的“不稳定 lambda”问题:在
PreferencesCard
的每次重组时,都会生成一对新的 lambda,这反过来会导致每个
CustomSwitch
可组合项的重组(lambda 被视为通过撰写为状态)。有一个技巧可以防止由于“不稳定的 lambda”而导致的重组,即传递方法引用来代替 lambda,但这些引用(通常指向 ViewModel 中定义的变异器)仍然必须从根可组合项向下传递,除非我们愿意将整个 ViewModel 作为参数传递(这是一个不好的做法)。
那么我们如何避免 prop 钻探,同时保持所有可组合项不可变?
internal val LocalPreferencesCardListener = compositionLocalOf {
IllegalStateException("Should be passed in root")
}
internal val LocalPreferencesCardState = compositionLocalOf {
IllegalStateException("Should be passed in root")
}
CompositionLocalProvider(
LocalPreferencesCardListener provides vm.getListener(),
LocalPreferencesCardState provides vm.getState(),
) {
NestedFun {
LocalPreferencesCardState.current //use for drawing state
NestedFun {
VeryNestedFun {
LocalPreferencesCardState.current //still use for drawing state
LocalPreferencesCardListener.current.invoke()
}
}
}
}