从 React 的角度看 Android 的 Jetpack C
最近为了开发一个小项目,学习了Jetpack Compose,API 设计的很不错。Jetpack Compose的API 非常丰富,正好我的 React 的知识可以发挥作用。也许这就是 React Native 开发者可以代替 Android 原生开发者的原因。
这两个框架的很多概念和方法虽然名称不同,但是工作原理却大同小异。以下是两者之间概念的对比和解释。
我们下面把 Jetpack Compose 简称为 JC。
Component 和 Composable
React 叫各个组成部分为 Component(组件)。
function Greeting(props) {
return <span>Hello {props.name}!</span>;
}
Jetpack Compose 叫各个组成部分为 Composable(其实也是组件)。Composable 方法除了需要是一个方法意外,还需要一个@Composable
注解。
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
Render 和 Composition
当一个组件包含的数据发生改变,这些变化的数据需要以定义好的方式绘制到屏幕上。React 的叫做 render,Jetpack Compose 的叫做 Compose。
Reconciler 和 Composer
React 内部需要找到组件发生变更的地方才能对应的绘制出来。这个算法叫做Reconciler。JC 也包含着这样的算法,执行这个算法的叫做 Composer。
State 和 State
React 和 JC 都把他们的状态叫做 State
useState 和 State
React 使用useState
来创建 state 变量。它会返回一个 tuple,一个是状态值,一个是这个状态的 setter。
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>You clicked {count} times</button>;
JC 使用mutableStateOf
方法返回一个MutableState
对象,这个对象还包含有一个属性和对应的 getter 和 setter。
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("You clicked ${count.value} times")
}
MutableState
可以像 React 的useState
一样返回 value 和对应的 setter。
val (count, setCount) = remember { mutableStateOf(0) }
Button(onClick = { setCount(count + 1) }) {
Text("You clicked ${count} times")
}
为了避免无效计算,remember
经常和mutableStateOf
一起使用。
setState 和 Snapshot
更新 React 的状态的时候可以使用一个方法来实现。如:
class Button extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
<button
onClick={() => this.setState((state) => ({ count: state.count + 1 }))}
>
You clicked {this.state.count} times
</button>;
}
}
在 JC 里,这个概念包含在一个叫做Snapshot
的类里面。这个类里面有一个enter
方法来调用更新后的回调。
Children Prop 和 Children Composable
React 和 JC 都把展示在其他 UI 组件里的组件叫做 Children。
React 是这样的:
function Container(props) {
return <div>{props.children}</div>;
}
<Container>
<span>Hello world!</span>
</Container>;
JC 是这样的:
@Composable
fun Container(children: @Composable () -> Unit) {
Box {
children()
}
}
Container {
Text("Hello world"!)
}
Context 和 CompositionLocal
数据知识沿着组件树传输有的时候过于繁琐。React 可以通过 Context 分享数据。JC 可以用CompositionLocal
来实现同样的目的。
createContext 和 compositionLocal
React 使用createContext
创建 Context 对象。JC 使用compositionLocalOf
和staticCompositionLocalOf
。
-
compositionLocalOf
只有子composition读取它的current
值的时候,会在compositionLocal的值改变的时候重绘 -
staticCompositionLocalOf
修改它的值,整个子lambda都会重绘,而不只是读取current
的值的地方重绘。
如果一个compositionLocal的不太会改变的话可以使用staticCompositionLocalOf
以获得性能的提升。
Provider 和 CompositionLocalProvider
<MyContext.Provider value={myValue}>
<SomeChild />
</MyContext.Provider>
JC 的实现:
CompositionLocalProvider(MyLocal provides myValue) {
SomeChild()
}
useContext 和 CompositionLocal.current
React:
const myValue = useContext(MyContext);
JC:
val myValue = MyLocal.current
总结一下 android 的写法:
使用composeLocalOf
或者staticCompositionLocalOf
(这个一般用于不怎么变化的值)创建一个对象。
val LocalColor = compositionLocalOf {Color.Red}
然后:
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalColor provides colors) {
// ... Content goes here ...
ProviderReaderCompositable {
Text(text="Read compositable", modifier = Modifier.background(LocalColor.current))
}
}
}
}
}
LocalColor
可以定义在MyActivity
这个文件引用过来,也可以定义在本文件内部,通过CompositionLocalProvider
把数据分享出去。
之后,在子组件中读取数据:
ProviderReaderCompositable {
Text(text="Read compositable", modifier = Modifier.background(LocalColor.current))
}
Hooks, Effect
React允许开发这写自己的 hooks,这样可以把逻辑抽离出来达到重用的效果。这些 hooks 里也可以用其他的 hooks 比如useState
和useEffect
。
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
}, [friendID]);
return isOnline;
}
JC 可以直接在@Composable
方法里实现这个功能:
@Composable
fun friendStatus(friendID: String): State<Boolean?> {
val isOnline = remember { mutableStateOf<Boolean?>(null) }
DisposableEffect(friendID) {
val handleStatusChange = { status: FriendStatus ->
isOnline.value = status.isOnline
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
onDispose {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
}
}
return isOnline
}
useEffect 和 LaunchedEffect
副作用(side effect)都是作为回调方法执行的。React 和 Jetpack Compose 都是如此。
useEffect(() => {
sideEffectRunEveryRender();
});
useEffect
的功能比较丰富。JC 把这些功能做了细分。比如:DisposableEffect
、LaunchedEffect
和SideEffect
。
Clean-up 方法和 DisposableEffect
组件不再使用的时候需要回收副作用不再使用的资源。React会在useEffect
里返回一个clean-up
方法:
useEffect(() => {
const subscription = source.subscribe(id);
return () => {
subscription.unsubscribe(id);
};
}, [id]);
JC 则使用DisposableEffect
。
DisposableEffect(id) {
val subscription = source.subscribe(id)
onDispose {
subscription.unsubscribe(id)
}
}
useEffect(promise, deps) 和 LaunchedEffect
JS 使用async
关键字创建异步方法。
useEffect(() => {
async function asyncEffect() {
await apiClient.fetchUser(id);
}
asyncEffect();
}, [id]);
上面的方法会在id
这个依赖项发生改变的时候访问 API 获取数据。React 没有内置取消 promise 的方法,但是可以使用AbortController
。如:
useEffect(() => {
const controller = new AbortController();
(async () => {
// The abort signal will send abort events to the API client
await apiClient.fetchUser(id, controller.signal);
})();
// Abort when id changes, or when the component is unmounted
return () => controller.abort();
}, [id]);
在 JC 里,使用的是suspend
方法和 coroutine。在LaunchedEffect
可以接受传入的参数,他们和 useEffect 里的依赖是同样的作用。如:
LaunchedEffect(id) {
apiClient.fetchUser(id)
}
useEffect(callback)和 SideEffect(callback)
useEffect
没有依赖的话,会在每次绘制之后执行。
useEffect(() => {
sideEffectRunEveryRender();
});
JC 使用SideEffect
实现同样的效果。
SideEffect {
sideEffectRunEveryComposition()
}
对于依赖的处理
总结以上,useEffect
可以有带依赖useEffect(() => {}, [deps])
, 可以不带依赖useEffect(() => {})
,还有一个就是可以带一个空数组当做依赖useEffect(()=> {}, [])
。
对应的,在 JC 里可以有
LaunchedEffect(keys=listOf(deps)) {
// Run when deps change
}
没有依赖:
SideEffect {
// Something...
}
依赖为空数组:
LaunchedEffect(Unit) {
// Run only once
}
在上面的一个例子中提到了自定义 hooks。如:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, (status) => {
setIsOnline(status.isOnline);
});
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID);
};
}, [friendID]);
return isOnline;
}
在上例中,使用了 JC 的DisposableEffect
。还可以选择另外一个方法:produceState
。如:
@Composable
fun friendStatus(friendID: String): State<Boolean?> {
return produceState(initialValue = null, friendID) {
ChatAPI.subscribeToFriendStatus(friendID) { status ->
value = status.isOnline
}
awaitDispose {
ChatAPI.unsubscribeFromFriendStatus(friendID)
}
}
}
在produceState
里可以直接对value
赋值,这样会调用他的 setter。读取的时候可以在返回值里读取。在produceState
里执行的可以是一个 coroutine(协程),或者使用awaitDispose
方法来清理资源。后者和useEffect
返回一个 clean-up 方法的做法类似。
Key pros 和 key Composable
处理列表显示的时候,React和 JC 都需要用到 Key prop。这样才能在这列表里那个发生了,更改、添加或者是删除。Key必须使用唯一值来标记列表里的元素。
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
JC有一个key
composable 可以使用:
Column {
for (todo in todos) {
key(todo.id) { Text(todo.text) }
}
}
.map 和 For 循环
React 经常使用 map 来显示一列组件:
function NumberList(props) {
return (
<ul>
{props.numbers.map((number) => (
<ListItem value={number} />
))}
</ul>
);
}
JC可以使用 for 循环,也可以使用 forEach,使用 map 也能达到效果:
@Composable
fun NumberList(numbers: List<Int>) {
Column {
for (number in numbers) {
ListItem(value = number)
}
}
}
使用 forEach 和 map 的例子就不写了,各位可以在测试项目里试试。
useMemo 和 remember
React 可以使用useMemo
来避免无效运算,只有在依赖发生变更的时候才执行运算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
JS 使用 remember
达到同样的效果。依赖可以作为参数传入:
val memoizedValue = remember(a, b) { computeExpensiveValue(a, b) }
条件绘制
React 里可以使用?:
操作符,比如:const a = x === y ? b : c
。再比如:
function Greeting(props) {
return (
<span>{props.name != null ? `Hello ${props.name}!` : 'Goodbye.'}</span>
);
}
对应的,JC 可以使用 kotlin 的语法来实现:
@Composable
fun Greeting(name: String?) {
Text(text = if (name != null) {
"Hello $name!"
} else {
"Goodbye."
})
}
预览
React 可以使用storybook
来实现。
JC 可以这样:
@Composable
@Preview(showBackground = true)
fun SettingsScreensPreview() {
MyTheme() {
SettingsScreen(null)
}
}