最近一直在学习 React,在看到 React Hooks 一章时联想到 Compose ,简直有着异曲同工之处,他们都是由 UI 组件、State 状态、Effect 副作用构成,而且,Android 端很多优秀的架构思路都来源于前端,适当性的学习些前端知识,反而更能容易理解当下 Android 原生的架构,这也是我一直推荐大家有时间也学习一下前端的原因,本期主要聊聊 Android 原生与 React 的对比,总结了类组件与函数组件的不同。
1、基于类组件的对比
原生
对于原生 Android 来说,通过 Activity 类来承载当前界面的 UI ,例如如下示例:
代码语言:javascript复制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 示例如下:
代码语言:javascript复制
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 界面的能力,如下是一个累加的组件:
代码语言:javascript复制@Composable
fun HomeWidget() {
var count by remember {
mutableStateOf(0)
}
Column {
Text(
text = "$count",
modifier = Modifier.clickable { count }
)
}
}
React
React 在 16.8 版本引入了 React Hooks,可以基于函数式来代替原来的类组件,如下也是一个累加的组件:
代码语言:javascript复制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,然后通知界面刷新:
代码语言:javascript复制@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 感知 组件挂载、组件更新的能力,例如模拟加载更多操作,触发加载更多就去请求网络数据:
代码语言:javascript复制@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 感知 组件挂载、组件更新、组件卸载的能力,例如监听好友在线状态能力:
代码语言:javascript复制@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 组件挂载、组件更新、组件卸载的能力,例如如下的定时组件
代码语言:javascript复制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]
参考资料
[1]
使用 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