Jetpack Compose 是什么
Jetpack Compose是Google推出的一个新的UI工具包,旨在帮助开发者更快、更轻松地在Android 平台上构建Native应用。Jetpack Compose是一个声明式的UI框架,它提供了现代化的声明式Kotlin API(取代Android 传统的xml布局),可帮助开发者用更少的代码构建美观、响应迅速的应用程序。
2019 年,Google 在 I/O 大会上公布了 Android 最新的 UI 框架:Jetpack Compose。Compose 可以说是 Android 官方有史以来动作最大的一个库了。它在 2019 年中就公布了,但要到今年也就是 2021 年才会正式发布。这两年的时间 Android 团队在干嘛?在开发 Compose。一个 UI 框架而已,为什么要花两年来打造呢?因为 Compose 并不是像 RecyclerView、ConstraintLayout 这种做了一个或者几个高级的 UI 控件,而是直接抛弃了我们写了 N 年的 View 和 ViewGroup 那一套东西,从上到下撸了一整套全新的 UI 框架。直白点说就是,它的渲染机制、布局机制、触摸算法以及 UI 的具体写法,全都是新的。
基于View UI体系有哪些痛点
历史包袱,10多个大版本的迭代,View类已经3w多行,而绝大部分的UI控件都继承于View。意味你写一个按钮或者一个TextView都会受这个父类影响,继承了很多没有用到的特性和功能;
解析xml的额外开销,而且需要反射创建对象 ;
预览和Reload不方便,和Flutter毫秒级的hot reload完全不能比;
布局嵌套层级过深导致的性能问题,比如LinearLayout 二次测量或者三次测量问题。
Compose特点
声明式
上面有一个词:声明式 ,那么什么是声明式?假设我们需要在界面 上显示一个文本
命令式方式:
1、首先需要一个xml文件,里面有一个TextView
...
<TextView
android:id="@+id/my_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
2、通过findViewById获取到TextView控件
TextView textView = findViewById<TextView>(R.id.my_text);
3、通过setText()更新数据,显示到界面
textView.setText(content);
声明式方式:
@Composable
fun Greeting() {
val count = remember { mutableStateOf(0) }
Column{
Button(onClick = { count.value++ }) {
Text("I've been clicked ${count.value} times")
}
}
}
为什么第一种方式是命令式,第二种方式是声明式?主要体现在界面更新上,命令式下:数据更新时,Java代码手动调用xml组件引用来更新界面,也就是Java代码命令xml界面更新,这就是命令方式。而声明式呢?只描述界面,当数据状态更新时,自动更新界面,这就是声明式。
简短总结:
命令式是操作界面 (How);
声明式是描述界面 (What)。
除了Jetpack Compose ,Flutter,React-Native,Swift-UI 都是声明式的,这也是现在的一种趋势。
强大的UI预览能力
顶层函数
Compose是一个声明式UI系统,其中,我们用一组函数来声明UI,并且一个Compose函数可以嵌套另一个Compose函数,并以树的结构来构造所需要的UI。在此过程中,Compose函数始终根据接收到的输入生成相同的UI,因此,放弃类结构不会有任何害处。从类结构构建UI过渡到顶层函数构建UI对开发者和Android 团队都是一个巨大的转变。
@Composable
fun checkbox ( ... ) //错误的命名,应该大写开头
@Composable
fun TextView ( ... )
@Composable
fun Edittext ( ... )
@Composable
fun Image ( ... )
Jetpack Compose首选组合而不是继承,Android中的几乎所有组件都继承于View类(直接或间接继承)。比如EidtText 继承于TextView,而同时TextView又继承于其他一些View,这样的继承结构最终会指向跟View
而Compose团队则将整个系统从继承转移到了顶层函数。 Textview , EditText , 复选框 和所有UI组件都是 它们自己的Compose函数,而它们构成了要创建UI的其他函数,代替了从另一个类继承。
重组
在命令式界面模型中,如需更改某个微件,您可以在该微件上调用 setter 以更改其内部状态。在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 -- 系统会根据需要使用新数据重新绘制函数发出的微件。Compose 框架可以智能地仅重组已更改的组件。重组整个界面树在计算上成本高昂,Compose 使用智能重组来解决此问题。
重组是指在输入更改时再次调用可组合函数的过程,Compose 可以高效地重组。
可组合函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。可组合函数应快速执行,以避免在播放动画期间出现卡顿。如果您需要执行成本高昂的操作(例如从共享偏好设置读取数据),请在后台协程中执行,并将值结果作为参数传递给可组合函数。
当您在 Compose 中编程时,有许多事项需要注意:
- 可组合函数可以按任何顺序执行;
- 可组合函数可以并行执行;
- 重组会跳过尽可能多的可组合函数和 lambda;
- 重组是乐观的操作,可能会被取消;
- 可组合函数可能会像动画的每一帧一样非常频繁地运行。
示例
和flutter比较像,https://flutter.cn/docs/development/ui/widgets-intro
/**
* Colume , Row ,Box
*/
@Preview(showBackground = true)
@Composable
fun DemoLayout() {
Row(
modifier = Modifier
.size(200.dp)
.background(Color.Yellow),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Box(
Modifier
.size(50.dp)
.background(Color.Red)
)
Box(
Modifier
.size(50.dp)
.background(Color.Blue)
)
Column(
Modifier
.size(100.dp)
.background(Color.Cyan)
) {
Text("Android")
Text(
"iOS"
)
Text(
"H5",
Modifier
.background(Color.Green),
fontSize = 15.sp
)
}
}
}
/**
* Text
*/
@Preview(showBackground = true)
@Composable
fun DemoText() {
val txt = remember { mutableStateOf(0) }
Text(
text = "${txt.value}",
Modifier
.background(Color.Magenta)
.size(200.dp, 200.dp)
.clickable(
enabled = true,
role = Role.Button
) {
txt.value += 1
},
fontStyle = FontStyle.Italic,
fontWeight = FontWeight(1000),
fontFamily = FontFamily.SansSerif,
letterSpacing = 10.sp,
textDecoration = TextDecoration.Underline,
textAlign = TextAlign.Center,
lineHeight = 20.sp,
maxLines = 3,
softWrap = true,
overflow = TextOverflow.Clip,
)
}
/**
* AppendText
*/
@Preview(showBackground = true)
@Composable
fun DemoAppendText() {
Text(
buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color.Blue,
fontWeight = FontWeight.Bold
)
) {
append("Jetpack ")
}
append("Compose ")
withStyle(
style = SpanStyle(
color = Color.Red,
fontWeight = FontWeight.Bold,
fontSize = 30.sp
)
) {
append("is ")
}
append("wonderful")
}
)
}
/**
* List
*/
@ExperimentalFoundationApi
@Preview(showBackground = true)
@Composable
fun DemoLazyColumn() {
Box {
val listState = rememberLazyListState()
LazyColumn(
Modifier.size(200.dp),
state = listState
) {
stickyHeader {
Text(text = "stickyHeader")
}
// Add a single item
item {
Text(text = "First item")
}
// Add 50 items
items(50) { index ->
Text(text = "Item: $index")
}
// Add another single item
item {
Text(text = "Last item")
}
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 10
}
}
Text(
text = if (showButton) {
"".plus(showButton)
} else {
"".plus(showButton)
},
modifier = Modifier
.size(30.dp)
.background(Color.Yellow)
)
}
}
/**
* Image, 图片库用coil : https://zhuanlan.zhihu.com/p/287752448
*/
@Preview(showBackground = true)
@Composable
fun ImageDemo() {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
)
}
/**
* Canvas
*/
@Preview(showBackground = true)
@Composable
fun CanvasDemo() {
Canvas(
modifier = Modifier
.height(300.dp)
.width(300.dp)
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawCircle(
color = Color.Blue,
center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
radius = size.minDimension / 4
)
drawLine(
start = Offset(x = canvasWidth, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Blue,
strokeWidth = 5F
)
drawLine(
start = Offset(x = 0f, y = 0f),
end = Offset(x = canvasWidth, y = canvasHeight),
color = Color.Blue,
strokeWidth = 5F
)
rotate(degrees = 45F) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = size / 3F
)
}
}
}
/**
* 手势
*/
@Preview(showBackground = true)
@Composable
fun GestureDemo() {
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
}
enum class BoxState { Collapsed, Expanded }
/**
* 动画, https://developer.android.com/codelabs/jetpack-compose-animation#3
*/
@Preview(showBackground = true)
@Composable
fun AnimatingBox(boxState: BoxState = BoxState.Expanded) {
val transitionData = updateTransitionData(boxState)
// UI tree
Box(
modifier = Modifier
.background(transitionData.color)
.size(transitionData.size)
)
}
// Holds the animation values.
private class TransitionData(
color: State<Color>,
size: State<Dp>
) {
val color by color
val size by size
}
// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 32.dp
BoxState.Expanded -> 300.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
遇到问题
1.如果要追踪具体的实现,需要反编译代码;
2.Preview功能还需要进一步增强,由于要实现实时预览,每次修改Compose都需要编译,如果项目比较大,编译时间很长,那体验就会很差了;
3.某些API设计上有些混淆,比如Text AlignText只能设置水平居中;
4.引入Compose会带来3M多的包大小。
总结
声明式UI使我们的代码更加简洁,这也是拥抱大前端一次很好的尝试。Compose 确实是一套比较难学的东西,因为它毕竟太新也太大了,它是一个完整的、全新的框架,确实让很多人感觉学不动,那怎么办呢?学呗
学习资料
2.View 嵌套太深会卡?来用 Jetpack Compose,随便套——Intrinsic Measurement
3.深入详解 Jetpack Compose | 优化 UI 构建