Android Jetpack Compose总结
简介
Jetpack Compose 是用于构建native UI的新方式,写法跟Flutter非常相似,对Flutter有了解的同学可以很快上手。
官网:https://developer.android.com/jetpack/compose
官方demo :https://github.com/android/compose-samples
官方的介绍: https://developer.android.com/jetpack/compose/setup
环境及版本
最低支持Android API 21,即5.0版本,必须使用kotlin语言,最低使用Android Studio 4.0 版本。
Jetpack Compose 目前处于实验阶段,现在是0.1-dev2,到1.0正式版估计还要一年时间。
后续版本可能会加入更多kotlin的特性,丰富动画等其他性能问题。
关于如何在现有项目中使用:
https://developer.android.com/jetpack/compose/setup#add-compose
怎么使用?
在AS 4.0中直接新建一个空的Compose项目,会有一个示例代码:
在函数前加@Compose
注解,就可以返回一个类似Flutter中的Widget的UI
加@Compose
注解的函数可以相互调用,这些函数会被插件编译处理,所以如果一个函数不是生成UI的,那么不要用此注解。@Preview
注解,可以在右边实时预览,改动函数后,刷新一个预览即可,添加该注解的外层函数不能有参数,但是里面可以嵌套一个带参数的函数来预览。可以在@Preview
后面添加一个名字,如:@Preview("Text preview")
关于Column 和Row 的概念跟Flutter中一样,包括主轴和副轴的概念大小如mainAxisSize
和对齐方式如crossAxisAlignment
,一段代码示例:
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) {
Column(crossAxisAlignment = CrossAxisAlignment.Center
crossAxisSize = LayoutSize.Expand,
mainAxisSize = LayoutSize.Expand) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
Divider(color = Color.Transparent, height = 32.dp)
Counter(counterState)
}
}
@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
可以使用HeightSpacer(24.dp)
或者WeightSpacer(24.dp)
来直接添加一个宽高间隔
按照官方的建议,我们可以把UI拆分成多个小的Compose函数,每个函数其实最终会被插件编译生成一个View,然后可以复用这些Compose函数,
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) {
Column(modifier = ExpandedHeight, crossAxisAlignment = CrossAxisAlignment.Center) {
Column(modifier = Flexible(1f), crossAxisAlignment = CrossAxisAlignment.Center) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(counterState)
}
}
在Column中,可以对参数modifier设置ExpandedHeight,类似于设置高度match_parent的意思,宽度同理。
关于如何使用Theme和自定义Theme
MaterialTheme中有很多颜色和字体样式,在外层包裹上MaterialTheme后,可以在内部的Compose函数中使用主题数据,如:style = +themeTextStyle { h1 }
@Composable
fun Greeting(name: String) {
Text (
text = "Hello $name!",
modifier = Spacing(24.dp),
style = +themeTextStyle { h1 }
color = +themeColor { surface }
)
}
通过使用copy函数可以在现有的一个主题上修改某一个属性值,如:
textStyle = (+themeTextStyle { body1 }).copy(color = Color.Yellow)
自定义Theme
import androidx.compose.Composable
@Composable
fun CustomTheme(children: @Composable() () -> Unit) {
// TODO
}
import androidx.compose.Composable
import androidx.ui.core.CurrentTextStyleProvider
import androidx.ui.graphics.Color
import androidx.ui.material.MaterialColors
import androidx.ui.material.MaterialTheme
import androidx.ui.text.TextStyle
val green = Color(0xFF1EB980.toInt())
val grey = Color(0xFF26282F.toInt())
private val themeColors = MaterialColors(
primary = green,
surface = grey,
onSurface = Color.White
)
@Composable
fun CustomTheme(children: @Composable() () -> Unit) {
MaterialTheme(colors = themeColors) {
val textStyle = TextStyle(color = Color.Red)
CurrentTextStyleProvider(value = textStyle) {
children()
}
}
}
Effects和memo
memo的作用:
1. 在recompositions(即该UI组件内部的Model数据变化时,该UI组件就会重新构建)的时候保存状态值,如下代码:
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) { ... }
上面的代码有一个问题,再重新构建的时候,原来的counterState数值就会丢失,每次都是一个新的counterState对象。
按照下面使用memo修改后,就可以解决问题:
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = +memo { CounterState() }
) { ... }
2. 在重组时,记住内部的一些计算结果,防止多次重复计算
如果在合成的中间需要进行计算,而又不想在每次重新组合函数时都进行计算,则可以记住该计算,即使重新组合了Composable函数,该计算也不会再次执行。
@Composable
fun Greeting(name: String) {
val formattedName = +memo { name.substringBefore(" ").toUpperCase() }
Text (
text = "Hello $formattedName!",
modifier = Spacing(24.dp),
style = +themeTextStyle { h3 }
)
}
@Preview
@Composable
fun DefaultPreview() {
MaterialTheme {
Greeting("Android 10")
}
}
比如这里的formattedName计算过程,在使用memo后,就不会重复计算,但是这样写有个bug,如果第二次调用时传入来了另外一个参数,那么由于memo复用原来的结果,就会导致bug,所以,对于需要修改的参数,可以以如下的方式来使用memo:
@Composable
fun Greeting(name: String) {
val formattedName = +memo(name) { name.substringBefore(" ").toUpperCase() }
Text (
text = "Hello $formattedName!",
modifier = Spacing(24.dp),
style = +themeTextStyle { h3 }
)
}
@Model注解
model注解标记一个数据类之后,在Compose函数中可以直接监听到数据变化,自动更新显示,
如:
定义:
@Model
class CounterState(var count: Int = 0)
使用:
@Composable
fun Counter(state: CounterState) {
Button(
text = "I've been clicked ${state.count} times",
onClick = {
state.count++
}
)
}
状态提升、 数据流向下传递、事件流向上传递
@Model
class FormState(var optionChecked: Boolean)
@Composable
fun Form(formState: FormState) {
Checkbox(
checked = formState.optionChecked,
onCheckedChange = { newState -> formState.optionChecked = newState })
}
在上面代码中,Checkbox的选中状态,在Checkbox和Form中都不保存,而改为由外部传入,原因是此时外部可能需要使用当前的状态值,那么由外部来创建并传递该参数到Compose函数中,这使得外部调用者提升了状态
⚠️注意:在可组合函数中,应该公开可能对调用函数有用的状态,因为这是可以使用或控制的唯一方法,称为状态提升。
状态提升的概念跟Flutter一样,后续应该也会像Flutter中的Provider、BLOC、或者Redux一样,推出相关的状态管理库,因为Compose + Model注解的方式,就是一种MVVM的思想,需要一种方便的数据状态管理的三方库来做这个事情。
关于数据流向: 父Composable函数可以控制其子数据。 子Compose UI不应从全局变量或全局数据存储中读取。Composable函数应仅接收所需信息,因此它们应尽可能简单,而不是调用父Composable函数可以提供的所有内容。
@Composable
fun MultipleGreetings(user: User = +memo { User("name", "surname") }) {
Column {
Greeting("${user.firstName} ${user.lastName}")
Greeting("Android 10")
Button(text = "Change name", onClick = {
user.firstName = "Changed"
user.lastName = "New surname"
})
}
}
@Composable
fun Greeting(name: String) {
val formattedName = +memo(name) { name.substringBefore(" ").toUpperCase() }
Text (
text = "Hello $formattedName!",
modifier = Spacing(24.dp),
style = +themeTextStyle { h3 }
)
}
比如上面代码中,Greeting从调用方Compose函数(MultipleGreetings)获取数据,作为参数传入,且Greeting只接收一个String,并不是整个User对象。
事件向上传递
事件通过lambda回调而不断往上。 当子Composable函数收到事件时,更改应传播回至关心该信息的Composable。
在我们的示例中,我们可以通过将Greeting的内容包装在以onClick侦听器为参数的Clickable函数(可在库中使用)中来使其可点击。 但是,Greeting是一个可重用的功能,它本身并不知道如何处理用户交互。 应该使用lambda将该信息从层次结构的底部(Greeting中的Clickable composable)传播到顶部的Composable函数,这些函数知道如何处理该信息,如以下示例所示:
@Composable
fun MultipleGreetings(user: User = +memo { User("name", "surname") }) {
val onClick = {
user.firstName = "Changed"
}
Column {
Greeting("${user.firstName} ${user.lastName}", onClick)
Greeting("Android 10", onClick)
Button(text = "Change name", onClick = onClick)
}
}
@Composable
fun Greeting(name: String, onClick: () -> Unit) {
val formattedName = +memo(name) { name.substringBefore(" ").toUpperCase() }
Clickable(onClick = onClick) {
Text (
text = "Hello $formattedName!",
modifier = Spacing(24.dp),
style = +themeTextStyle { h3 }
)
}
}
Greeting通过调用父级作为参数传递的lambda告诉MultipleGreetings它被单击了。 如果您运行该应用程序,则可以看到在任何问候语文本上进行点击都会传播更改,并且顶部的Greeting实例将重新组合。
Data flow in Compose apps. Data flows down with parameters, events flow up with lambdas.
Compose和现有的View互操作
Compose写的函数可以用在xml中,Android现有的View也可以用Compose的方式来写,如:
总结: Compose借鉴了Flutter和Swift UI的编写方式,代码简洁,可以实时预览效果,截止到2019年11月19日,目前版本才为0.1,预计正式发布1.0后,会有更多功能更新,日常的一个小demo可以先使用Compose熟悉起来。
参考:
https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/#0