关于状态提升 (State Hoisting) 和单向数据流 (UDF)想再补充个实战场景加深印象
之前这篇文章[Jetpack Compose] AndroidView最后有个AndroidView 的实战案例,正好昨天也整理了 state 的相关内容,就拿代码直接来重新巩固下:
@Composable
fun MyScreen() {
// 定义一个状态:当前的网址
var currentUrl by remember { mutableStateOf("https://google.com") }
Column {
Button(onClick = { currentUrl = "https://github.com" }) {
Text("切换到 GitHub")
}
AndroidView(
// factory 只跑一次 new 一个 WebView 出来
factory = { context ->
WebView(context).apply {
// 比如在这里配置一些固定不变的设置
settings.javaScriptEnabled = true
}
},
// update 会在 currentUrl 变化时,自动重新执行!
update = { webView ->
// 每次 currentUrl 变了,这里就会拿着原来的 webView,去 load 新的 Url
webView.loadUrl(currentUrl)
}
)
}
}
上面代码就是“状态提升 (State Hoisting)”和“单向数据流 (UDF)”的完美体现:
- 父组件(
MyScreen)紧紧握住数据(currentUrl) - 子组件(
Button和AndroidView)各司其职,一个负责触发事件去修改数据,一个负责被动接收数据来刷新 UI。完全做到了单一职责,互不干扰。
有个经常会提到的底层细节:状态改变时,到底是谁被重新执行了?
一、重组作用域 (Recomposition Scope)
在我之前的理解中:“currentUrl 变了,所以 Compose 框架会直接回调 AndroidView 的 update 方法”。
在底层机制上,并不是直接去调 update 的,而是触发了更大范围的“重组”,然后顺藤摸瓜执行到 update。
1.1 谁真正监听了 currentUrl?
这里需要纠正一个初次学习 compose 很容易踩的坑:Column 不是真正的父类
在 Compose 中,只有非内联(non-inline)的 @Composable 函数才能形成一个“重组作用域(Recomposition Scope)”。
- 代码里的
Column组件,点进源码看,其实是一个inline(内联) 函数。 - 因为它是内联的,所以在编译器的视角里,
Column是不存在的,它里面的代码被直接铺平在了MyScreen里。 - 所以真正监听到
currentUrl变化、并在底层注册了监听器的,是MyScreen这个父组件本身。
1.2 状态改变后的“顺藤摸瓜”
当 Button 被点击,currentUrl 的值发生改变。由于 MyScreen 读取了这个值,Compose 框架会说:
MyScreen的数据过期了,MyScreen你给我重新执行一遍!
于是,MyScreen 这个函数开始从头到尾重新跑:
-
跑到
Button:Compose 框架会检查传给Button的参数变了没有。因为Button这里并没有用到currentUrl的值(它只传了一个死的回调逻辑),参数没变,Compose 非常聪明地触发智能跳过(Smart Recomposition),Button内部不重组。 -
跑到
AndroidView:Compose 发现,传给AndroidView的update参数(那个 Lambda 表达式)里面包着最新的currentUrl。因为参数变了,AndroidView内部会被触发更新。它会跳过factory(因为只跑一次),然后重新执行update这个 Lambda,把新传入的 url load 进去。
1.3 最佳实践
这种设计的模式,是官方强烈推荐的最佳实践。
以前用 Java 写这段逻辑,大概是在 Button 的点击事件里,直接写一句 webView.loadUrl("...")。这就是命令式编程(直接让 A 去操作 B)。但在 Compose 中,Button 根本不知道 WebView 的存在,这两者是完全解耦的。
Button 只负责改 State,WebView 只负责读 State。
二、总结
在 Compose 中,我们极其推崇将状态提升到父组件中管理:子组件不应该互相持有引用或直接通信,而是通过读取共享的 State 来实现联动。
当 State 改变时,Compose 会触发读取了该 State 的最近的 Recomposition Scope(如父组件)进行重组。
在重组过程中,利用 Compose 编译器的‘智能跳过(Smart Recomposition)’机制,只有参数真正发生变化的子组件(如本文案例中的
AndroidView)才会重新执行更新逻辑,而其他无关组件(如Button)会被安全跳过。这种设计既保证了单向数据流的纯粹性,又兼顾了极高的渲染性能。