众所周知Jetpack Compose设计理念甚至团队成员很多都来自React,在API方面参考了很多React(Hooks) 的设计,通过与React进行对比可以更好地熟悉Compose的相关功能。
Compose目前处于alpha版,虽然API还会调整,但是从功能上已经基本对齐了React,不会有大变化,本文基于1.0.0-alpha11
。
<br/>
React Component
vs Composable
React中Component成为分割UI的基本单元,特别是16.8之后Hooks引入的函数组件,相对于类组件更利于UI与逻辑解耦。函数组件是一个接受Props作为参数并返回JSX node的函数:
function Greeting(props) {
return <span>Hello {props.name}!</span>;
}
Compose同样使用函数作为组件:添加了@Composable
注解的函数。而且借助Kotlin的DSL实现声明式语法,而无需额外引入JSX等其他标记语言,相对于React更加简洁:
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
<br/>
JSX
vs DSL
DSL相对于JSX更加简洁,可以直接使用原生语法表示各种逻辑。
loop
例如在JSX中实现一个循环逻辑,需要两种语言混编
function NumberList(props) {
return (
<ul>
{props.numbers.map((number) => (
<ListItem value={number} />
))}
</ul>
);
}
DSL中的循环就是普通的for
循环
@Composable
fun NumberList(numbers: List<Int>) {
Column {
for (number in numbers) {
ListItem(value = number)
}
}
}
If statement
JSX 使用三元运算符表示条件
function Greeting(props) {
return (
<span>
{props.name != null
? `Hello ${props.name}!`
: 'Goodbye.'}
</span>
);
}
DSL直接使用IF表达式
@Composable
fun Greeting(name: String?) {
Text(text = if (name != null) {
"Hello $name!"
} else {
"Goodbye."
})
}
key component
React和Compose都可以通过key来标记列表中的特定组件,缩小重绘范围。
JSX使用key属性
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
DSL使用key组件来标识Component
Column {
for (todo in todos) {
key(todo.id) { Text(todo.text) }
}
}
<br/>
Children Prop
vs Children Composable
前面提到,React与Compose都使用函数组件创建UI,区别在于一个使用DSL,另一个依靠JSX。
React中,子组件通过props的children字段传入
function Container(props) {
return <div>{props.children}</div>;
}
<Container>
<span>Hello world!</span>
</Container>;
Compose中,子组件以@Composable函数的形式传入
@Composable
fun Container(children: @Composable () -> Unit) {
Box {
children()
}
}
Container {
Text("Hello world"!)
}
<br/>
Context
vs Ambient(CompositionLocal)
对于函数组件来说,建议使用props/parameter传递数据,但是允许一些全局数据在组件间共享。React使用Context存放全局数据,Compose使用Ambient
(alpha12中已改名CompositionLocal
)存放全局数据
createContext : ambientOf
React使用createContext
创建Context:
const MyContext = React.createContext(defaultValue);
Compose使用ambientOf
创建Ambient:
val myValue = ambientOf<MyAmbient>()
Provider : Provider
React和Compose中都使用Provider
注入全局数据,供子组件访问
<MyContext.Provider value={myValue}>
<SomeChild />
</MyContext.Provider>
Providers(MyAmbient provides myValue) {
SomeChild()
}
useContext : Ambient.current
React中子组件使用useContext hook
访问Context
const myValue = useContext(MyContext);
Compose中子组件通过单例对象访问Ambient
val myValue = MyAmbient.current
<br/>
useState
vs State
无论React还是Compose,状态管理都是至关重要的。
React使用useState hook
创建State
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>
You clicked {count} times
</button>
Compose使用mutableStateOf
创建一个state,还可以通过by代理的方式获取
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("You clicked ${count.value} times")
}
还可以通过解构分别获取get/set
val (count, setCount) = remember { mutableStateOf(0) }
Button(onClick = { setCount(count + 1) }) {
Text("You clicked ${count} times")
}
或者通过by
代理
var count : Int by remember { mutableStateOf(false) }
Button(onClick = { count++ }) {
Text("You clicked ${count} times")
}
Compose创建state时往往会remeber{ }
避免重绘时的反复创建state,相当于useMemo
<br/>
useMemo
vs remember
React使用useMemo hook
用来保存那些不能随重绘反复计算的值,只有参数变化时才会重新计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Compose中同样功能使用remember
实现,同样通过参数作为重新计算的判断条件
val memoizedValue = remember(a, b) { computeExpensiveValue(a, b) }
<br/>
useEffect
vs SideEffect
函数组件满足纯函数的要求:无副作用、无状态、即使多次运行也不会产生影响。但是总有一些逻辑不能以纯函数执行,例如 生命周期回调、日志、订阅、计时等,只能在特定时机执行,不能像一个纯函数那样可以执行多次而不产生副作用。
React中,useEffect
提供一个hook点,会在每次render时执行。注意 这不同于直接写在外面,当diff没有变化时不需要重新render,就不需要执行useEffect了
useEffect(() => {
sideEffectRunEveryRender();
});
Compose中使用SideEffect
处理副作用(早期版本是onCommit{ }
)
SideEffect {
sideEffectRunEveryComposition()
}
useEffect(callback, deps) :DisposableEffect
跟useMemo一样可以接受参数,每次render时,只有当参数变化时才执行:
useEffect(() => {
sideEffect();
}, [dep1, dep2]);
只在第一次render时执行的逻辑(相当于onMount),可以使用如下形式处理:
useEffect(() => {
sideEffectOnMount();
}, []);
Compose中使用DisposableEffect:
DisposableEffect(
key1 = "",
...
) {
onDispos{}
}
Clean-up function : onDispose
useEffect通过返回一个function进行后处理
useEffect(() => {
const subscription = source.subscribe();
return () => {
subscription.unsubscribe();
};
});
DisposableEffect通过一个DisposableEffectDisposable
进行后处理:
DisposableEffect() {
val dispose = source.subscribe()
onDispose { //返回DisposableEffectDisposable
dispose.dispose()
}
}
<br/>
Hook
vs Effect
React允许自定义Hooks封装可复用逻辑。Hooks可以调用useState、useEffect等其他hooks放方法,在特定的生命周期完成逻辑。自定义Hooks都使用useXXX的形式来命名
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
Compose没有命名上的要求,任何一个@Composable函数即可被用来实现一段可复用的处理Effect的逻辑:
@Composable
fun friendStatus(friendID: String): State<Boolean?> {
val isOnline = remember { mutableStateOf<Boolean?>(null) }
DisposableEffect {
val handleStatusChange = { status: FriendStatus ->
isOnline.value = status.isOnline
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
onDispose {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
}
}
return isOnline
}