初识Jetpack Compose

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预览能力

image.png
image.png
image.png

顶层函数

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多的包大小。


image.png

总结

声明式UI使我们的代码更加简洁,这也是拥抱大前端一次很好的尝试。Compose 确实是一套比较难学的东西,因为它毕竟太新也太大了,它是一个完整的、全新的框架,确实让很多人感觉学不动,那怎么办呢?学呗

学习资料

1.Compose官网

2.View 嵌套太深会卡?来用 Jetpack Compose,随便套——Intrinsic Measurement

3.深入详解 Jetpack Compose | 优化 UI 构建

4.官方视频-Jetpack Compose Beta 版现已发布!

5.Jetpack Compose 使用前后对比

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 何为Compose 2019 年中,Google 在 I/O 大会上公布的用于Android构建原生界面的全新 U...
    塞上牧羊空许约阅读 569评论 0 2
  • 发表时间:2019.11.7 在前不久的 Android Dev Summit '19 上,Jetpack Com...
    Loong_T阅读 1,047评论 1 1
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,173评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,033评论 0 4