使用Jetpack Compose构建Android UI

Jetpack Compose

Jetpack Compose 是一个独立的 UI 工具包,它结合了响应式编程模型和 Kotlin 编程语言的简洁性和易用性,旨在简化 UI 开发。
它是完全声明性的,意味着可以通过调用一系列将数据转换为UI的函数来描述UI。当基础数据更改时,框架会自动调用这些函数,从而更新视图层次结构。
现在的版本还是 0.1.0-dev02,处于非常早期的版本,官方也再三强调非常有可能产生变化且无法用于生产环境。不过简单了解下 Compose 还是不错的。

1. 准备

要启动新的Compose项目,请打开Android Studio 4.0,然后选择启动新的Android Studio项目:


Android Studio版本

创建新项目时,从可用模板中选择“
Empty Compose Activity”,注意
minimumSdkVersion 至少为21及以上,“Language” 必须为kotlin:

创建项目

2. Jetpack Compose构建UI的特点

API

Button 继承自 TextView,理论上我们只需要一个文本 + 可点击的区域就可以了,但是由于 TextView 的特性,它本身是可以长按出现复制、选择功能的,但是一个 Button 要这些功能有什么用呢?Jetpack Compose 的核心: 组合优于继承,所有的 UI 都是通过组合实现,不存在继承关系。

Code

目前的 UI 构建方式来说,写一个自定义 View 需要实现测量和布局,响应用户的行为需要实现大量的 Listener 事件,同时还要配合 XML 自定义属性,非常繁琐。而且以目前的View代码量体积来说,想要完全优化重构是不现实的。发布一个全新的 UI 构建库,从根本上解决问题,所以 Google 推出了全新的 Android UI 组件库 Jetpack Compose。

Jetpack Compose 试图改变原有的 UI 构建方式,同时带来以下 4 点全新的改变:


Goals
  1. UI 的变化更新不再跟随 Android 大版本的发布而更新
  2. 编写 UI 代码不需要掌握庞大繁琐的技术栈
  3. 简单直接的状态控制以及用户行为处理
  4. 使用更少的代码来编写 UI

说了这么多,用一下看看吧。

3. 使用Compose构建UI

新创建好的MainActivity长这样:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview
@Composable
fun DefaultPreview() {
    MaterialTheme {
        Greeting2("Android")
    }
}

使用setContent用来定义布局,但不是使用XML文件,而是在其中调用Composable函数。要创建可组合函数,只需将@Composable注释添加到函数。该函数可以调用其他的@Composable函数。

@Composable
fun Greeting(name: String) {
   Text(text = "Hello $name!") //Text是library提供的可组合函数。
}

可组合函数是带有@Composable注释标记的Kotlin函数

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    Greeting(name = "Android")
}

@Preview标记任何一个无参数的Composable函数并Build项目,就可以在Android Studio中看到预览。

Text preview

刷新UI

遵循单一职责原则。@Composable函数负责单个功能,该功能完全由该函数封装。例如,如果要为某些组件设置背景色,则必须使用Surface可组合功能。

Text设置背景色,我们需要定义一个Surface包裹它。

@Composable
fun Greeting(name: String) {
    Surface(color = Color.Yellow) {
        Text (text = "Hello $name!")
    }
}
Text preview

Modifiers
Modifiers是为UI组件提供其他修饰的属性列表。目前可用的修饰符有:SpacingAspectRatio和修改Flexible Layouts布局的RowColumn

@Composable
fun Greeting(name: String) {
    Surface(color = Color.Yellow) {
        //Spacing 为文本添加填充
        Text(text = "Hello $name!", modifier = Spacing(24.dp)) 
    }
}

点击Build & Refresh按钮查看预览:

Modifiers

请注意,@Composable注释仅对创建UI的函数是必需的。它可以调用常规函数和其他Composables函数。如果某个功能不满足这些要求,则不应使用@Composable注解。

创建通用Container

@Composable
fun MyApp(child: @Composable() () -> Unit) {
    MaterialTheme {
        Surface(color = Color.Yellow) {
            child()
        }
    }
}

该函数以Composable函数(在此称为)的 lambda 作为参数,该 lambda child返回Unit。我们返回Unit是因为所有Composable函数都必须返回Unit

@Composable()将 Composable 函数用作参数时,需要添加注解:
fun MyApp(child: @Composable() () -> Unit) { ... }

后面代码就可以如此调用:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                Greeting("Android")
            }
        }
    }
}

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    MyApp {
        Greeting("Android")
    }
}

将UI组件提取到Composable函数中,以便我们可以重复使用它们而无需复制代码。比如使用不同的参数重用同一Composable函数。以垂直顺序排列,我们使用ColumnComposable函数(类似于垂直LinearLayout)。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                MyScreenContent()
            }
        }
    }
}

@Composable
fun MyScreenContent() {
    Column {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
    }
}

@Composable
fun Greeting(name: String) {
    Text (text = "Hello $name!", modifier = Spacing(24.dp))
}

@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
    MyApp {
        MyScreenContent()
    }
}

Divider 是提供的可组合函数,用于创建水平分隔线。

preview

可以像Kotlin中的任何其他函数一样调用compose函数。可以添加语句来影响UI的显示方式,构建UI非常方便。

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
    }
}
preview

不知道你有没有这种想法,这里的for循环会不会就是把 Text 翻译为 TextView,然后此方法就是接收一个 List<String> 对象,返回一个 List<TextView>?
显示布局边界看下:


preview

事实并非如此,它所有的可绘制元素都不是 Android 原生的 View,其顶层View为AndroidComposeView,内部在维护的 ComponentNode负责绘制。


AndroidComposeView

数据流
通过将对象作为参数传递给Composable函数,数据向下流动。

@Composable 
fun MyExampleFunction(items: List<Item>) {
    Column {
        for (item in items) {
            RenderItem(item = item)
        } 
    }
}

@Composable
fun RenderItem(item: Item) {
    Row {
        Text(text = item.name)
        WidthSpacer(4.dp)
        Text(text = item.description)
    }
}

RenderItem从调用Composable函数接收其所需的数据作为参数。如果我们要处理Item单击,则使用lambda 将信息从层次结构的底部传递到顶部。

@Composable 
fun MyExampleFunction(items: List<Item>, onSelected: (Item) -> Unit) {
    Column {
        for (item in items) {
            RenderItem(item = item, onClick = { onSelected(item) })
        } 
    }
}

@Composable
fun RenderItem(item: Item, onClick: () -> Unit) {
    Clickable(onClick = onClick) {
        Row {
            Text(text = item.name)
            WidthSpacer(4.dp)
            Text(text = item.description)
        }
    }
}
数据随参数向下流动,事件随lambda向上流动

数据随参数向下流动,事件随lambda向上流动。

使用@Model管理状态

对状态更改做出反应是Compose的核心。如果数据发生更改,则可以使用新数据调用Composable函数将数据转换为UI,从而更新UI。
Compose使用自定义的Kotlin编译器插件,当基础数据发生更改时,可以重新调用函数以更新UI视图。
Compose提供了@Model注解,该注解可以放在任何类上。如果数据发生更改,从@Model参数读取值的可组合函数将自动被调用。该@Model注解将导致编译器重写类,使它可观察和线程安全。可组合函数将自动订阅它读取的类的任何可变变量。如果它们发生变化,将重新组合读取这些字段。
举个栗子,比如做一个计数器,跟踪用户单击多少次Button:

@Model 
class CounterState(var count: Int = 0)

在CounterState加上注解@Model,任何将此类作为参数的Composable函数在count值更改时将自动重新组合。定义Counter为一个Composable函数,该函数采用CounterState一个参数,并发出Button,显示单击了多少次。

@Composable 
fun Counter(state: CounterState) {
    Button(
        text = "I've been clicked ${state.count} times",
        onClick = {
            state.count++
        }
    )
}

每次count更改时,Button都会重新构成并显示的新值count。

@Composable
fun MyScreenContent(
    names: List<String> = listOf("Android", "there"),
    counterState: CounterState = CounterState()
) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
        Divider(color = Color.Transparent, height = 32.dp)
        Counter(counterState)
    }
}
Model

感觉有种JS上Object.setProperty的即时感,确实如官方所说, Jetpack Compose 受到了 React、Litho、Vue、Flutter 的启发。

布局

列和行的主轴和横轴

与屏幕中心对齐,我们可以使用列的crossAxisAlignment参数:

@Composable
fun MyScreenContent(
    names: List<String> = listOf("Android", "there"),
    counterState: CounterState = CounterState()
) {
    Column(crossAxisAlignment = CrossAxisAlignment.Center) {
        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()
    }
}

刷新预览:


preview
@Composable
fun Counter(state: CounterState) {
    Button(
        text = "I've been clicked ${state.count} times",
        onClick = {
            state.count++
        },
        style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
    )
}
preview

Compose 对 ConstraintLayout 的支持还正在进行中。对于现有的布局控件,以后应该都是会添加支持的。

Compose 提供了 VerticalScroller 和 HorizontalScroller 来生成列表,在使用上与 RecyclerView 是完全不同的体验:

@Composable
fun MyApp() {
    VerticalScroller {
        Column {
            repeat(20) {
                Row(mainAxisSize = LayoutSize.Expand) {
                    Container(height = 48.dp) {
                        Text("Item $it", modifier = Spacing(left = 16.dp))
                    }
                }
            }
        }
    }
}

事实上,Scroller 与 ScrollView 更接近,只提供了一个滚动的功能,并没提到有对 View 进行回收复用。

兼容现有UI的构建方式
上图:

GenerateView

使用 Jetpack Compose 编写的 View,可以无缝的通过 xml 在原有视图上使用,只需要增加一个 @GenerateView 注解。

GenerateView

原有的 View 也支持 Jetpack Compose 写法。目前在预览版里@GenerateView注解还无法使用,不免有些遗憾~

自定义view

自定义view

@Preview
@Composable
fun errorView() {
    val checkBox = @Composable {
        Draw { canvas: Canvas, parentSize: PxSize ->
            val size = parentSize.width.value
            val outer = RRect(0f,0f,size,size).withRadius(Radius(10f, 10f))
            canvas.drawRRect(outer, Paint().apply {
                color = Color.Red
            })
        }

        Draw { canvas: Canvas, parentSize: PxSize ->
            val paint = Paint().apply {
                color = Color.White
                strokeCap = StrokeCap.round
                strokeWidth = 10f
                isAntiAlias = true
            }
            val size = parentSize.width.value
            val leftStart = Offset(size / 4, size / 4)
            val leftEnd = Offset(size / 4 * 3, size / 4 * 3)
            val rightStart = Offset(size / 4 * 3, size / 4)
            val rightEnd = Offset(size / 4, size / 4 * 3)
            canvas.drawLine(leftStart, leftEnd, paint = paint)
            canvas.drawLine(rightStart, rightEnd, paint = paint)
        }
    }
    Layout(children = checkBox) { _, _->
        layout(IntPx(200), IntPx(200)){}
    }
}

Jetpack Compose 中实现自定义 View 的过程也非常简单,我们只需要关注 Draw 和 Layout 这两个方法就好了,绘制过程和之前一样,还是经过 measure、layout、draw ,但写法很精简。

Jetpack Compose带给我们一种Android新的构建UI方式的实践,从语法上来看还是有些Flutter的影子,声明式UI和数据驱动带给我们更多想象力。在未来的计划中,Jetpack Compose 会支持 Kotlin 协程、会支持现有的 Android Arch Componet 、会有更完善的动画机制。一起期待吧~

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