如何用 Jetpack Compose 开发一个页面?

491次阅读  |  发布于1年以前

本文简单介绍了下如何使用 Compose 开发一个简单的页面,这里面包含了一些基本的要素:定义组件、更新状态、预览页面。有关 Compose 的架构,以及它的渲染原理,可以期待以后的文章。

Compose 出来也有一段时间了,刚 1.0.0 发布的时候简单看过一些,却一直没有尝试。最近 GDG 突然举办了一个 Compose 学习挑战赛,还有奖品,初级回答几个问题就可以拿到周边,进阶挑战赛还有机会得到一本 Compose 的书,便参加了这场学习挑战赛,而且最近写 Java 实在多,正好写一写 Kotlin 转换一下心情。

Compose是什么

“Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。”这是来自官方的描述,总之就是一个 Android Framework 扩展包,用一种新的方式来写 Native UI。它看上去,就长这样:

是不是感觉非常眼熟?这 Scaffold,这 AppBar,还有 Column,这不就是 Flutter 的 API 么?!看着这个代码就非常地好理解了,如果你熟悉 Kotlin,又写过 Flutter 或者 React,Compose 基本上就是 0 成本上手。这段代码,对应到 Android XML 差不多就是长这样:

<Scaffold
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <TopAppBar
    android:title="MyApp Title"/>
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
      android:layout_width="match_parent"
      android:layout_height="match_content"
      android:onClick="handleClick"
      android:text="Ciallo~"/>
  </LinearLayout>
</Scaffold>

如何开发一个页面

简单介绍下 Compose 里一些基本的要素,有了这些,就可以写一个简单的页面了!

写一个 Composable 组件

在 Compose 中,所有的组件,都是一个函数,这个函数使用 @Composable 注解标识。只有使用这个注解标识的函数才会被认为一个 Compose 组件,所有 Compose 组件也只能在 @Composable 标识的函数中使用。下面这段代码就是自定义了两个 Compose 组件。

与 Flutter / React 一样,Compose 中也万物皆组件,一个组件可以是一个页面,也可以被其它组件使用。


/**
 * 定义了一个 HomePage 组件
 * 该组件使用了一个官方的 Box 容器,以及自定义的 Body 组件
 */
@Composable
fun HomePage() {
    Box {
        Body(title = "HomePage") {
            Text(text = "content")
        }
    }
}

/**
 * 定义了一个 Body 组件
 * 组件本身可以传递参数,函数参数即为组件的参数
 * 参数也可以是一个 Composable 组件,因此可以向组件传递另一个组件
 * 从而达到类似 Flutter / React children 的效果
 */
@Composable
fun Body(title: String, content: @Composable () -> Unit) {
    Column {
        Text(text = title)
        content()
    }
}

Compose 的 DSL 里大量应用了 Kotlin 的语法特性,最基本的就是将函数当作参数传递以及尾闭包。在 Kotlin 中,若函数的最后一个参数是也是一个函数,则这个参数传递时可以写在函数调用后面,于是就看起来像是函数调用后面加了一对大括号。

关联 Activity

组件写好了,接下来就可以将它显示出来了。Compose 仍然依赖于 Android Framework 的 Activity 体系,本文章中不探究 Compose 组件是如何渲染的,这里可以简单的认为,所有的 Compose 组件其实只是 Kotlin DSL 的产物,它仍然在 View 体系中渲染(实际上这并不准确)。因此,要把 Compose 组件像 View 一样,通过 setContent 方法挂载到 Activity 上。

/**
 * 必须继承 ComponentActivity
 */
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 调用 setContent 函数
        setContent {
            HomePage()
        }
    }
}

到这里为止,其实就可以将 App 运行起来了。

逻辑 & 状态

到目前为止,我们写出来的页面只是一个静态页面,但如果要处理业务逻辑,改变 UI 的样式该怎么办?原生 Android 通过 findViewById 拿到 View,然后设置 View 中的属性,内部调用 requestLayout 触发下一帧的重绘,那 Compose 该怎么做? Compose 是一个声明式、响应式的 UI 框架。不像原生 Android 需要通过命令式的代码调用,显示地去改变某个 View 的状态。在 Compose 中,只需要使用“状态”,将状态与组件的属性绑定,在触发逻辑时,改变状态值即可触发 UI 刷新。这一点,其实原生 Android View 体系中也可以做到了,有兴趣的同学可以去看下 Android ViewModel + dataBinding 写 UI 的方式。本质上它其实是帮我们向 ViewModel 中的 LiveData 注册了观察者,属性变化时,调用了 View.setXXX 方法。

Android dataBinding XML 和 ViewModel 示例,来自我的 GitHub 仓库中三年前的项目。

Compose 中可以让这种数据绑定变得更简单,只需要分三步,按下面的代码:


/**
 * 定义了一个 Body 组件
 * 组件本身可以传递参数,函数参数即为组件的参数
 * 参数也可以是一个 Composable 组件,因此可以向组件传递另一个组件
 * 从而达到类似 Flutter / React children 的效果
 */
@Composable
fun Body(title: String, content: @Composable () -> Unit) {
    // 1. 创建一个状态
    var stateTitle by remember {
        mutableStateOf("default state")
    }

    Column {
        // 2. 将状态值做处理,赋值给 Text.text 属性
        Text(text = "${title}_${stateTitle}")
        Button(onClick = {
            // 3. 改变状态
            stateTitle += "?"
        }) {

        }
        content()
    }
}

remember 和 mutableStateOf 都是 Compose 提供的函数,用于在组件中创建一个状态,mutableStateOf 创建了一个 WrapperDelegate,有 setValue 和 getValue 方法,而 remember 则将 MutableState 作为状态存储,当改变此状态值时,Compose 会进行 Recomposition 操作,重新执行 Body 函数,也就是刷新组件。

这里还用到了 Kotlin 的 by 关键字 Delegate 特性,可以去官网查阅这部分内容(不得不说 Kotlin 语法真多)。

预览

Android XML 写完是可以预览的,而且速度非常快,还有可视化编辑,但 Compose 又怎么样呢?由于 Compose 实际上依赖 Kotlin 代码的编译,在预览这一块确实是不如 XML 了。在 Compose 中你得给需要预览的组件写上 @Preview 注解。


/**
 * 定义了一个 HomePage 组件
 * 该组件使用了一个官方的 Box 容器,以及自定义的 Body 组件
 */
@Composable
@Preview(showBackground = true)
fun HomePage() {
    Box {
        Body(title = "HomePage2333") {
            Text(text = "content")
        }
    }
}

Compose Preview 的缺点就是慢,代码改变之后需要 rebuild 才能生效,但它具备 XML 更完善的功能,例如可以选择 interactive mode,这样可以在 Preview 下响应代码逻辑,也可以针对单个组件直接运行 App,不需要整个运行,这在调试单个组件时很有用。(当然不管哪个都还是不如 Flutter 的调试体验好就是了)

一个简单的计算器

仿 小米 12 pro 上的计算器!参加 Compose 学习挑战赛时候写着玩的。要求支持基本的加减乘除,如果能适配横屏加分。这里附上 GitHub 仓库链接,有兴趣的同学可以查看完整源码,这里只简单介绍动画和横屏适配的部分。计算器多少有很多 BUG,核心的表达式解析部分也是两年前学编译原理时候写的脚本解析器,全部都是 BUG。

Compose 计算器:https://github.com/yumeTsukiiii/ComposeCalculator

算术脚本解析器:https://github.com/yumeTsukiiii/atri_script

动画

视频中主要涉及到按键以及文字的缩放动画,动画其实也是通过状态驱动的,本质上,Compose 的动画 API,就是类似创建了一个 Interval 去改变状态的值,不断触发重绘。下面是按键缩放的动画逻辑:

// 按键 Scale 动画状态
@Composable
fun MutableInteractionSource.animationScale(): Float {
    // 1. 通过 MutableInteractionSource.collectIsPressedAsState 获取 isPress 手势状态
    val isPressed by collectIsPressedAsState()
    // 2. animateFloatAsState 创建 Float 类型的状态值
    // 当 isPressed 变化时,它将从旧的值逐渐变化到新的值
    val animationScale by animateFloatAsState(targetValue = if (isPressed) 0.7f else 1.0f)
    return animationScale
}

@Composable
fun ScaleAnimationButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    // 3. 通过 Modifier.scale 设置 scale 的值
    // 当 animationScale 返回值发生变化时,这个组件将重绘。
    Button(onClick, modifier.scale(interactionSource.animationScale()), enabled, interactionSource, elevation, shape, border, colors, contentPadding, content)
}

横屏适配

之前看 Google I/O 2022 时,里面有介绍 Compose 如何做大屏适配,说实话这个方法感觉也不是很好用,需要通过 if 判断去实现不同的布局,并且在不用全局状态管理的情况下还需要将 windowSize 往下传递,代码量会比较多。


class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeCalculatorTheme {
                // 1. 获取 windowSizeClass
                CalculatorApp(windowSizeClass = calculateWindowSizeClass(activity = this))
            }
        }
    }
}

@Composable
fun CalculatorApp(
    windowSizeClass: WindowSizeClass
) {
    // 2. 判断当前是否为大屏(宽大于高)
    if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
        // 横屏布局
    } else {
        // 竖屏布局
    }
}

总结

本文简单介绍了下如何使用 Compose 开发一个简单的页面,这里面包含了一些基本的要素:定义组件、更新状态、预览页面。真实场景下使用 Compose 会非常复杂,比如页面级以及全局状态管理、导航、数据存储等,这些都需要结合 Jetpack 架构组件来使用。(虽然这和我目前工作所用到的技术和这些完全不搭边)

有关计算器的实现没有写太多,这个里面并没有用到很多 Compose 其他的知识,由于时间限制也只是用 MutableState + Component 写了很简单的 UI,没有去设计和整理。

后续可能会更新一些文章介绍下 Compose 中如何像自定义 View 那样,自己控制绘制和布局,以及它是如何渲染的等等。

最后放一个参加 GDG Compose 初级挑战赛活动获得的奖品,写计算器进阶挑战赛奖品估摸着是拿不到,毕竟大佬云集,咱只是赶工写着玩玩。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8