使用Jetpack Compose构建Android UI

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

1. 准备

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


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

2. Jetpack Compose构建UI的特点

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

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

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


  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中看到预览。


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

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

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

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按钮查看预览:

请注意,@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 是提供的可组合函数,用于创建水平分隔线。

可以像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)
        }
    }
}

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



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


数据流
通过将对象作为参数传递给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向上流动。

使用@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)
    }
}

感觉有种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()
    }
}

刷新预览:


@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)
    )
}

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的构建方式
上图:

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

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

自定义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 、会有更完善的动画机制。一起期待吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章