如何避免在 Jetpack Compose 中进行螺旋钻探

Mis*_*ith 10 android immutability kotlin android-jetpack-compose prop-drilling

我将尝试用一个简短的例子来描述这个问题。

假设我们有一个代表自定义开关的子可组合项,并且希望保持它不可变,因此我们需要传递初始状态和 lambda,以便在用户切换开关时更改其真实来源:

@Composable
fun CustomSwitch(
    title: String? = null,
    checked: Boolean = false,
    onSwitchChanged: ((Boolean) -> Unit)? = null
){
    //...
}
Run Code Online (Sandbox Code Playgroud)

现在我们有了一个父可组合项,代表屏幕上有许多开关的部分。如果我们也想保持它不可变,我们需要通过参数向上公开子级的所有属性:

@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
}    
Run Code Online (Sandbox Code Playgroud)

免责声明:在这个例子中,这个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
}
Run Code Online (Sandbox Code Playgroud)

从状态的角度来看,这很好,因为 Compose 足够智能,仅当子参数更改时才更改子级(具有不可变属性的数据类是稳定的)。但对于 lambda,我们可能会遇到臭名昭著的“不稳定 lambda”问题:每次重新组合 时PreferencesCard,都会生成一对新的 lambda,这反过来会导致每个CustomSwitch可组合项的重新组合(Compose 将 lambda 视为状态)。

有一个技巧可以防止由于“不稳定的 lambda”而导致的重组,即传递方法引用来代替 lambda,但这些引用(通常指向 ViewModel 中定义的变异器)仍然必须从根可组合项向下传递,除非我们愿意将整个 ViewModel 作为参数传递(这是一个不好的做法)。

那么我们如何避免 prop 钻探,同时保持所有可组合项不可变呢?