Jetpack Compose Concepts Every Developer Should Know
作者:Clinton Teegarden
什么是Compose
Jetpack Compose是Android新的声明式UI框架。Android开发早已熟悉用xml写UI,在视图的层级中穿梭来更新这些带状态的视图。而Jetpack Compose,通过使用Kotlin的函数编写的是无状态的UI。
Composable函数通过添加@Composable的注解。Composable的函数必须加上这个注解,这样编译器才能知道这个函数是往视图层级里面添加UI。Composable函数本身只能被其他Composable函数调用,但是它们自己可以调用其他标准函数。
如果你还没开始用Compose,我强烈推荐你去看看这里的给Compose学习资料,这里提供了一些基础的细节和示例可以让你快速开始你的Compose之旅。
单项数据流
Compose 是基于单项数据流来构造的,这样一来Compose的编程模型对合理实现Compose有比较大的要求。跟传统的Android UI体系不同的是,Composable相对来说说是无状态的——也就是说UI的展示状态应该通过Composable函数自己的参数传递进去。
在大部分基础的场景下,不管事件(events)是从UI来(按钮点击,文本输入等等)或者其他地方传来(API调用,回调等等)都是通过handler来处理,完了传递给Composable函数去更新UI的状态。因为Composable是没有状态的,会根据提供的UI状态来构建UI。
在上面的流程图里面,UI层就是Composable。事件从这一层出发,比如按钮点击传递给Event Handler,比如ViewModel。ViewModel通过LiveData/StateFlow进行UI状态控制。随着状态的改变,这些更新会被推送给Composable用最新更新的状态进行重组。
下面的代码演示了上面单项的数据流——从ViewModel收集状态,将事件发送给ViewModel,然后通过ViewModel进行状态更新。
MVI_ComposeTheme {
Surface(color = MaterialTheme.colors.background) {
val state = viewModel.viewState.collectAsState().value
Button(onClick = { viewModel.processEvent()}) {
Text(state.message)
}
}
}
组合和重组
对于用户组合就是Composable函数执行和UI创建的过程。重组就是随着Composable用于展示的状态和数据发生变化的时候更新UI的过程。在重组的过程中,compose能知道是哪个Composable用到的数据发生了变化,并只更新有变化的UI组件。其他不变的Composable则跳过。
在整个生命周期里面,组合和重组其实是不同的。
- Composable函数可能一帧都会进行重组,例如动画
- Composable函数可能会有任意执行顺序
- Composable函数可能会并行执行
这就意味着不能包含当组合函数里面执行时要做的逻辑——有时候称为副作用。
Compose_Theme {
MainScreen()
// DO NOT DO THIS
viewModel.makeAPICall()
}
有状态的Composable和保存实例状态告别
虽然我们的目标是让Composable主要是无状态的,但是有时候我们也需要部分是有状态的,比如记住滚动状态,在Composable之间共享一个变量等等。因为重组每一帧都可能发生,如果每次重组就会丢掉滚动的位置就不好了。
我们可以在Composable里面通过创建变量来记录这个值:
val myInt = remember{ Random(10).nextInt() }
在上面的例子里,这个随机生成的整数会在组合的过程被记住二不需要计算。如果这个整数没有被remember包裹,每次重组都会重新计算。
更进一步,我们在配置发生变化的时候也能记住。
val myInt = rememberSaveable{ Random(10).nextInt() }
有时候我们需要这样一个记住的变量,当一个Composable更新导致另一个Composable的重组。在下面这个例子里,按钮点击会增加计数器,这个计算器被Text Composable用来展示。因此,当按钮点击的时候,我们想Text显示内容随着计算增加而变化。
为了实现这个目标,我们会像上面的例子一样用到Compose提供的remember函数,但是这个整数会被MutableState的对象包装。MutableState类会持有一个单一值,读写都可以被Compose监听到,并且会导致相关的Composable发生重组。
Column{
// create state for buttonCount and a function to update it -
// setButtonCount
val (buttonCount, setButtonCount) =
rememberSaveable { mutableStateOf(0) }
Button(onClick = {
setButtonCount(buttonCount + 1)
}) {
Text(text = "Press Me!")
}
// recomposes whenever button is pressed
Text(text = "Button Pressed $buttonCount")
}
槽(slot)API
Compose引入了槽API的概念。这可以让Composable实现高度定制,而不需要Composable函数提供大量的实现去做多种多样的定制UI。因为每个场景和实现可能不同,槽API支持在Composable里用空的slots来定制UI。
例如,有很多按钮需要不进展示文本,有的需要展示加载条,有的在左边展示图标,有的图标展示在右边。槽API支持这种多样性的定制Composable。
其他的Composable,类似Scaffolds完全就是用槽实现的。想象一下用这些保留间隔的各种各样UI组件来构建你的UI会是什么样的,这些组件有toolbar,bottomNav,Drawer,Screen Content等等。
修饰器(Modifier)
Modifier可以同xml的属性进行类比,通常用来设置UI的样式。但是modifier更简单,还有一些奇技淫巧。modifier可以让你装饰和修改Composable的默认实现。修改外观,添加无障碍信息,处理UI交互事件等等都可以通过modifier来实现。由于modifier只是Kotlin的对象,因此也可以添加到自定义的Modifier。
Text(text = "Your Text", modifier = Modifier.padding(5.dp))
Modifier是是非常强大的,因为它提供了实现Composable层而不需要嵌套到其他Composable的可能。例如下面这个例子,在Android传统UI里面不嵌套很多View是无法实现的。
但是,用Modifier用一个Composable就能实现。这是因为Modifier是有序的,通过padding,color的不同顺序实现了不同UI的组合。
Text(text = "Fake Button",
modifier = Modifier.padding(5.dp)
.background(Color.Magenta)
.padding(5.dp)
.background(Color.Yellow))
懒加载列表(Lazy List)
LazyLists是一个类似RecyclerViews的Compose。我会跟你说,我不会再怀念之前写RecyclerView Adapters, ViewHolders还有其他一些相关的模板代码了。下面这个例子展示了LazyList的列形式(纵向滚动)基于奇数偶数展示不同的UI元素。如果用RecyclerView的话,我们需要一个Adapter,至少两个不同的ViewHolder。而用Compose,我们只需要一个LazyColumn 的列来动态添加内容的Composable函数。
val listSize = 100
LazyColumn {
items(listSize) {
if (it % 2 == 0) {
Text("I am even")
}else{
Text("I am odd")
}
}
}
就是这个了,这可能是我用了Compose的最开心的事情吧。
约束布局(Constraint Layout)
Compose有它自己的约束布局版本,这个我们在传统UI里面所熟悉和喜爱的。约束布局在传统UI系统里面是强烈推荐的,因为它可以实现高度定制UI的方式通过相对于其他View来布局二不需要嵌套。有了Compose之后,嵌套不再是一个问题了,但是有时候还是需要一些工具比如constraints, barriers, weights等等约束布局去提供。
ConstraintLayout {
// Creates references for the three Composables
val (button1, button2, text) = createRefs()
Button(
// constraintAs is like setting the ID, required.
modifier = Modifier.constrainAs(button1) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button 1")
}
// constraintAs is like setting the ID, required.
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button1.bottom, margin = 16.dp)
centerAround(button1.end)
})
// Create barrier to set the right button to the right of the
button or the text, which ever is longer.
val barrier = createEndBarrier(button1, text)
Button(
modifier = Modifier.constrainAs(button2) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(barrier)
}
) {
Text("Button 2")
}
}
Compose和Navigation
Jetpack Compose里面的Navigation可以实现很多我们习惯用的Jetpack Navigation。但是Jetpack Compose可以让我们在同一个Activity里面进行Navigate不同页面,二不需要多个fragment。
只需要创建一个NavHost嵌套一个Screen Composable到里面。
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home"){
val vm: HomeVM = viewModel()
HomeScreen(vm)
}
composable("settings"){
val vm: SettingsVM = viewModel()
SettingsScreen(vm)
}
composable("profile"){
val vm: ProfileVM = viewModel()
ProfileScreen(vm)
}
}
通过androidx.lifecycle:lifecycle-viewmodel-compose
可以在Composable内创建ViewModel:
val vm: MyVM = viewModel()
Composable内部创建的ViewModel会一直保留直到它们的scope(Activity/Fragment)被销毁。这让你可以像现在同fragment一样来同NavHost Screen。在我的例子里面,我是在NavigationHost里面创建的ViewModel,这是是一个例子方便你去了解它的用法。
把NavHost应用于BottomNav或者其他Navigation组件,你可以用Scaffold:
Scaffold(
bottomBar = {
BottomNavigation {
// your navigation composable here
}
},
) {
NavHost(
navController = navController,
startDestination = "home") {
// your screen composables
}
}
小结
上面的概念只是Compose给我们提供的一个介绍。Compose 相对于Android开发者经常用来构建UI的方式有很大不同,但是确实一个很高兴看到的改变,因为它大大简化了传统UI体系的很多问题。如果你还没开始用Compose,我强烈推荐你去看看这里的给Compose学习资料。虽然非常长,但是里面每个字都值得你去花时间。祝你有一个愉快的Compose之旅。