Android Jetpack Compose

Android Jetpack Compose是谷歌推出的一种新的搭建UI的方式,使用Kotlin DSL的形式来组合UI组件。目前还在alpha阶段。苹果早也推出了类似的Swift UI,都是模仿前端的实现方式,目的都是UI更加轻量化和方便数据驱动。

Compose使用感受

Compose的使用比较简单,官方有连续的课程,还有比较完善的samples可以参考,网络上也能找到好多相关的教程,就不再重复写了。需要注意的是,Compose还在alpha阶段,API还未稳定,网上的教程好多是针对旧的API的,遇到问题还是尽量查看官方文档。下面就简单说说在使用Compose时的一些感受。

好的地方:

  1. 相对于之前的xml方式,代码编写起来可能更加熟练,对开发可能更亲切(个人感受)
  2. 一般的UI组件使用起来更加简单,如Text:
 Text(
    "text",
    style = MaterialTheme.typography.body,
    modifier = Modifier.padding(start = 4.dp) 
)

相对于xml中声明TextView,代码量会小很多。

  1. 可能是由于目前Compose提供的组件比较少,布局实现比较单一,感觉实现比较复杂的布局比XML高效,简洁很多。而且性能也比XML高,毕竟少了xml解析的过程。
  2. 可以多个UI组件一起预览。传统方式一次只能预览一个xml布局,使用Compose之后,只要在Compose上添加@preview注解,在一个文件内都可以预览。在搭建UI时几乎可以不再需要安装到真机和模拟器上进行检查。
  3. 数据驱动UI更新更加直观和高效,只需要声明state即可,数据改变时会自动更新UI,不再需要之前的Livedata等比较复杂的机制。
  4. Theme更加强大和自由,以前在XML中声明Theme和自定义主题中某个组件的样式,是比较麻烦的,因为属性太多了。现在就比较方便了可以直接使用Material Design主题中定义好的样式,更加规范高效,而且深色主题适配更加简单。
  5. 动画API更加简单了,不再需要复杂的写法,会自动根据之前的属性生成对应的动画,如更改组件的大小,只需要在modifier中添加:
modifier.animateContentSize(animSpec = TweenSpec(300)),

就可以了,系统会自动监控这个组件的大小的变化,生成动画。

不好的地方

  1. 由于使用的是Kotlin DSL,所以代码排版缩进会比较多,相对于一般代码结构,直观性比较差。但是还是比Flutter的好一些。
  2. 由于要实现实时预览,每次修改Compose都需要编译,如果项目比较大,编译时间很长,那体验就会很差了
  3. 组件的丰富度还比较欠缺,需要进一步完善
  4. 官方的教程,文档包含的内容有限,有很多之前的组件找不到在Compose中对应的组件,命名也发生了变化。寻找起来比较麻烦。大部门只能看官方的文档。而且对复杂的界面布局的实现没有比较完善的指导文档。
  5. jetpack组件还在推广中,Compose已经和好多AndroidX的组件冲突了,如果Compose推进的比较快,那还有必要学习使用AndroidX中的UI和管理UI组件吗? 一般的项目也不会想两套共存吧? 所以如何推广Compose还是个问题, 好处是不像iOS的Swift UI那样,和系统版本绑定。

Compose的实现

最初看到Compose的时候,以为就是对之前的View组件做了一次封装,然后底层再做组装,渲染处理。后来看了下源码,发现不是这样的,是重新实现了一套。如Text的具体实现是CoreTextCoreText的layout实现:

Layout(
        children = if (inlineComposables.isEmpty()) {
            emptyContent()
        } else {
            { InlineChildren(text, inlineComposables) }
        },
        modifier = modifier
            .then(controller.modifiers)
            .then(
                if (selectionRegistrar != null) {
                    Modifier.longPressDragGestureFilter(
                        longPressDragObserver(
                            state = state,
                            selectionRegistrar = selectionRegistrar
                        )
                    )
                } else {
                    Modifier
                }
            ),
        minIntrinsicWidthMeasureBlock = controller.minIntrinsicWidth,
        minIntrinsicHeightMeasureBlock = controller.minIntrinsicHeight,
        maxIntrinsicWidthMeasureBlock = controller.maxIntrinsicWidth,
        maxIntrinsicHeightMeasureBlock = controller.maxIntrinsicHeight,
        measureBlock = controller.measure
    ) 

TextController控制Text的layout、state、selection、measure和draw。底层其实都是通过TextDelegate实现的

    fun layout(
        constraints: Constraints,
        layoutDirection: LayoutDirection,
        prevResult: TextLayoutResult? = null,
        respectMinConstraints: Boolean = false
    ): TextLayoutResult {
        val minWidth = if (respectMinConstraints || style.textAlign == TextAlign.Justify) {
            constraints.minWidth.toFloat()
        } else {
            0f
        }
        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
            constraints.maxWidth.toFloat()
        } else {
            Float.POSITIVE_INFINITY
        }

        if (prevResult != null && prevResult.canReuse(
                text, style, maxLines, softWrap, overflow, density, layoutDirection,
                resourceLoader, constraints
            )
        ) {
            return with(prevResult) {
                copy(
                    layoutInput = layoutInput.copy(
                        style = style,
                        constraints = constraints
                    ),
                    size = computeLayoutSize(constraints, multiParagraph, respectMinConstraints)
                )
            }
        }

        val multiParagraph = layoutText(
            minWidth,
            maxWidth,
            layoutDirection
        )

        val size = computeLayoutSize(constraints, multiParagraph, respectMinConstraints)
        return TextLayoutResult(
            TextLayoutInput(
                text,
                style,
                placeholders,
                maxLines,
                softWrap,
                overflow,
                density,
                layoutDirection,
                resourceLoader,
                constraints
            ),
            multiParagraph,
            size
        )
    }

layout最终返回一个TextLayoutResultTextLayoutResult在draw的时候使用:

fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
       TextPainter.paint(canvas, textLayoutResult)
}

最终调用了TextPainterTextPainter的paint方法:

 fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
        val needClipping = textLayoutResult.hasVisualOverflow &&
            textLayoutResult.layoutInput.overflow == TextOverflow.Clip
        if (needClipping) {
            val width = textLayoutResult.size.width.toFloat()
            val height = textLayoutResult.size.height.toFloat()
            val bounds = Rect(Offset.Zero, Size(width, height))
            canvas.save()
            canvas.clipRect(bounds)
        }
        try {
            textLayoutResult.multiParagraph.paint(
                canvas,
                textLayoutResult.layoutInput.style.color,
                textLayoutResult.layoutInput.style.shadow,
                textLayoutResult.layoutInput.style.textDecoration
            )
        } finally {
            if (needClipping) {
                canvas.restore()
            }
        }
    }

设计思想还是View的那套,但是所有组件的实现更加扁平了。不再有View或Viewgroup的继承关系,大部分组件都是直接自己直接实现layout,measure和draw。所以看起来更加简洁。

State

Compose的更新数据显示是通过State来实现的,而且比之前的LiveData更加简单,如:
数据:

val list = listOf(
    "ListItem1“, 
    "ListItem2“,
    "ListItem3“,
    "ListItem4“,
    "ListItem5“
)

val data by remember{ mutableStateOf(list) }

UI:

LazyColumnFor(
     items = data,
     modifier = Modifier.weight(1f),
     contentPadding = PaddingValues(top = 8.dp))
{ text ->
       Text(text = text)
 }

添加数据:

data.value = list += listOf("ListItem6")

添加之后列表会自动刷新数据和UI。不需要跟之前一样去主动notify UI更新。
其中remember{}的表达的意思是跟字面意思一致,就是记住后面block中产生的value, 只有在UI组合时才会产生值。
mutableStateOf产生一个SnapshotMutableState,里面的value的读和写都是被监控的。在UI组合完成之后Composer会一直监控state, 如果有值发生变化则会触发Recomposition, 进行UI更新。

Recomposition

官方翻译为重组,就是重新调用compose组合UI的过程,系统会根据需要使用新数据重新绘制函数发出的组件。因为UI是一直显示的,重组可能会很频繁,为了保证流畅性,官方做了很多优化,如并行处理,自动跳过不需要重组的组件和使用乐观算法优化重组。总之和之前的UI实现一样,不要在compose组合期间执行比较耗时的逻辑。

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