为什么说 Compose 的灵感来自 React
The following article is from 扣浪 Author codelang
最近一直在学习 React,在看到 React Hooks 一章时联想到 Compose ,简直有着异曲同工之处,他们都是由 UI 组件、State 状态、Effect 副作用构成,而且,Android 端很多优秀的架构思路都来源于前端,适当性的学习些前端知识,反而更能容易理解当下 Android 原生的架构,这也是我一直推荐大家有时间也学习一下前端的原因,本期主要聊聊 Android 原生与 React 的对比,总结了类组件与函数组件的不同。
1、基于类组件的对比
原生
对于原生 Android 来说,通过 Activity 类来承载当前界面的 UI ,例如如下示例:
class HomeActivity extends Activity{
private var textView:TextView ?= null
private val vm:ViewModel by viewModels<XXXViewModel>()
fun onCreate(){
textView = findViewById(R.id.textview);
// 发起请求
vm.request();
vm.observer(this){
textView.text = it // 更新 UI
}
}
}
这还是一个比较简单的例子,当业务越来越复杂,最后你会发现,虽然项目是按照 MVVM 结构来写,但依然控制不住整个 Activity 充斥着各种请求和 observer。当然,也有人用 MVI 的方式来解决这个问题。
React
React 相比较原生而言会有点不同,虽然都是基于类组件开发,但 React 是基于 React.Component,它更像是原生里面的 View,继承自这个 View 来写各种逻辑,然后再将 View 设置到 XML 中,供 Activity 来加载绘制,他们之间的关系就像这样:
但 React.Component 相比较 View 又拥有更丰富的生命周期:
生命周期 | React.Component | 原生 View |
---|---|---|
组件挂载 | componentDidMount() | onAttachedToWindow() |
组件更新 | componentDidUpdate() | 无 |
组件卸载 | componentWillUnmount() | onDetachedFromWindow() |
... | ... | 无 |
React 示例如下:
class HomeWidget extends React.Component {
...
render() {
return (
<div>
<button>"Hello World"</button>
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<HomeWidget/>);
与 React 类组件非常相似的还有 Flutter,这两者可以对比着学习
2、基于函数组件的对比
原生
原生在拥有 Jetpack Compose 之后,也具备了像前端那样,基于函数式组件来描述当前 UI 界面的能力,如下是一个累加的组件:
@Composable
fun HomeWidget() {
var count by remember {
mutableStateOf(0)
}
Column {
Text(
text = "$count",
modifier = Modifier.clickable { count++ }
)
}
}
React
React 在 16.8 版本引入了 React Hooks,可以基于函数式来代替原来的类组件,如下也是一个累加的组件:
function HomeWidget() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click</button>
</div>
);
}
结合 Compose 与 React 函数组件的对比来看,两者区别不大,例如 State 状态的对比:
React | Compose | |
---|---|---|
State 状态 | useState() | mutableStateOf() |
那函数式组件相比较类组件拥有哪些好处呢?
更轻量,不用去写 class 代码更简洁,逻辑更内聚
但函数式组件还有一个问题需要解决,在类组件中,我们有原生 Activity 的 onCreate、onDesotry 等生命周期函数,在 React.Component 中,我们有 componentDidMount、componentWillUnmount 等生命周期函数,那基于函数式的组件,他是如何在函数中感知生命周期呢?那这就要引入 Side-effects(附带效应) 概念了。
3、基于附带效应的对比
对于函数副效应来说,赋予组件拥有如下三种生命周期感知能力即可:
组件挂载 组件更新 组件卸载
原生
Compose 提供了多个 Effect,但这里我们主要讲两个涉及到生命周期的 Effect
LaunchedEffect DisposedEffect
这两者的功能对比如下:
Effect | 可感知的生命周 | 是否支持协程 | 能力 |
---|---|---|---|
LaunchedEffect | 组件挂载、组件更新 | 支持 | 在组件中更安全的调用挂起函数,退出组合时会自动取消协程 |
DisposedEffect | 组件挂载 、组件更新 、组件卸载 | 不支持 | 可以监听组件的退出 |
1、模拟 LaunchedEffect 仅感知组件挂载的能力,例如请求网络获取到数据后设置给 state,然后通知界面刷新:
@Composable
fun HomeWidget() {
var response by remember {
mutableStateOf("")
}
LaunchedEffect(true) {
// todo 模拟请求网络
delay(1000)
response = "hello world"
// 打印 log
Log.e("TAG", "LaunchedEffect")
}
Column {
Text(text = response),
// ... 省略累加控件
}
}
在进入组合项时,LaunchedEffect 设置为 true,使其不具备监听任何状态变化的能力(remember),在延迟 1s 后会打印 Log,之后无论怎么操作其他控件都不会使其响应。除非组合项卸载并重进进入挂载状态才会触发,例如移除组件,然后又重新添加了该组件这种情况。
2、模拟 LaunchedEffect 感知 组件挂载、组件更新的能力,例如模拟加载更多操作,触发加载更多就去请求网络数据:
@Composable
fun HomeWidget() {
var count by remember {
mutableStateOf(0L)
}
LaunchedEffect(count) {
// todo 模拟请求网络
delay(1000)
Log.e("TAG", "count = $count")
}
Column {
Button(onClick = { count++ }) {
Text(text = "模拟加载更多")
}
}
}
在组合项进入挂载状态时,Log 会打印 count = 0
,在触发模拟加载更多后,count 值发生变化,LaunchedEffect 感知到状态发生变更,则会继续触发 网络请求,这时会打印 count = 1
,这就是感知组件更新的能力。这里有一点需要注意,如果不停的去点击 count 的话,仅最后一次才会触发 Log,因为每次启动 LaunchedEffect 前,Compose 都会取消上一次还未结束的协程(delay),这也是 LaunchedEffect 启动协程安全的原因
3、模拟 DisposedEffect 感知 组件挂载、组件更新、组件卸载的能力,例如监听好友在线状态能力:
@Composable
fun OnlineWidget(vm: OnlineViewModel = viewModel()) {
// 当前所有用户
val users = vm.users
var currentUser by remember { mutableStateOf(users[0])}
DisposableEffect(key1 = currentUser) {
vm.registerListener(currentUser)
Log.e("TAG", "注册 $currentUser 在线状态")
onDispose {
vm.unregisterListener(currentUser)
Log.e("TAG", "反注册 $currentUser 在线状态")
}
}
Column {
Text(text = "用户 $currentUser 的在线状态是 ${vm.isOnline}")
LazyColumn() {
items(users) { user ->
Button(onClick = {
currentUser = user // 切换用户
}) {
Text(text = "user = $user")
}
}
}
}
}
DisposableEffect 与 LaunchedEffect 不同,DisposableEffect 的闭包是 DisposableEffectScope,而 LaunchedEffect 的闭包是 CoroutineScope,所以,DisposableEffect 无法像 LaunchedEffect 做一些耗时操作,它更适合去做一些监听与反监听的注册操作,来避免潜在的内存泄漏问题。
DisposableEffect 提供了 onDispose 来感知监听状态的卸载操作,如上在切换用户时,会触发 onDispose 卸载上一次的用户监听,并重新注册新的用户进行监听。如果 OnlineWidget 整个组件在界面上被移除了,onDispose 依然能监听到并触发反注册。
React
React 相比较 Compose 而言会更好理解一点,只需理解 useEffect 即可,他更像是 LaunchedEffect 和 DisposableEffect 的结合,既可以处理耗时操作,也可以感知组件挂载、更新、卸载状态。
1、模拟 useEffect 组件挂载、组件更新、组件卸载的能力,例如如下的定时组件
function TimeoutWidget() {
const [value, setData] = useState(0)
useEffect(() => {
const timeout = setTimeout(() => {
setData(value + 1)
}, 1000)
return () => clearTimeout(timeout);
}, [value])
return <div>
<h1>{value}</h1>
</div>
}
组件挂载阶段时,useEffect 初始化 setTimeout 每隔 1s 执行一次,并监听 value 状态的变化,在 1s 结束触发 setData 累加 value 值,这时候,value 只发生变化,将会执行 return 的 clearTimeout 函数,清除定时器,然后重新执行 useEffect 函数继续注册定时监听,在 TimeoutWidget 组件被界面移除时,也会执行 clearTimeout 操作
小结
基于副效应的函数组件,React 和 Compose 都能通过一个函数来替代原来类组件的开发方式,但对于 Compose 来说,仅仅监听组件的 挂载、更新与卸载 往往是不够的,手机端与 PC 端不同,手机端有一些特殊的逻辑需要在息屏与亮屏的时候做一些操作,这是 PC 不会有的场景,所以,对于 React 来说,这三种足够满足业务诉求的开发,对于 Jetpack Compose 来说,官方也考虑到了这种情况,如下是官网监听 onStart、onStop 的示例:
参考资料:
使用 Effect Hook – React[1] Compose 中的附带效应[2]
使用 Effect Hook – React: https://react.docschina.org/docs/hooks-effect.html
[2]Compose 中的附带效应: https://developer.android.com/jetpack/compose/side-effects?hl=zh-cn#disposableeffect
推荐阅读探索 Compose 内核:深入 SlotTable 系统 J神出品!Molecule - 让 Compose 从此摆脱 ViewModel
Jetpack Compose 自定义 Layout 详解
Jetpack Compose 自定义 Layout 详解