Jetpack Compose快速上手

Jetpack Compose是安卓最新的UI工具,使用Kotlin以声明式的风格写界面。本文希望从Compose出发,通过较详细全面的讲解示范,介绍安卓开发的一些基本方法,解释一般文章或AI回复中不太会认真解释的问题,希望可以帮读者实现自己的想法,做出满意的安卓App。

环境配置

建议使用Android Studio开发,选择Compose模板即可开始。 API等级和构建工具可以保持默认(API 29,Kotlin DSL),有域名的话可以把域名反过来作为包名替代默认的com.example.myapplication

Android Studio建议使用JetBrains Toolbox下载和管理。

本文使用的Android Studio版本

选择带Compose图标的这个空模板

界面编写

经过漫长的初始化和同步(视网络情况而定),等你看到右下角的进度条消失,项目就加载好了。 此时左上角的文件结构显示类型会变成Android,这个布局会用更少的层级展示项目文件。 展开app查看更细致的分类,其中manifest文件夹存放配置文件,写有权限、活动(Activity)等,如果解压过apk文件可能见过。 kotlin+java是主要代码,大多数时候都在这里工作,实现应用功能。 res存放各种资源文件,如图片、字符串等。用Java开发时还需要经常在这里定义菜单、界面等一堆麻烦事,但有了Compose就不需要啦。 下面的Gradle Scripts存放各种构建配置,例如依赖、Java版本、编译SDK。

我们直接关注安卓应用的核心代码,即项目默认展示的这个,MainActivity.kt

// MainActivity.Kt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyDroidTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MyDroidTheme {
        Greeting("Android")
    }
}

这就是整个页面背后的代码啦,如果你不熟悉Kotlin,可以在Learn X in Y minutes上扫一眼。总的来说,Kotlin是一门非常优雅简便的语言,甚至有些代码可能一时难以看懂,不过那些情况我们遇见时会讲的,先从这个主页学起。

就算你从没有接触过安卓开发,或许也可以看出,第一块代码是应用的主活动类,继承了库提供的某个类,并重写了它的onCreate方法。方法实现首先开启了EdgeToEdge,它可以让系统的状态栏和应用融合更好,你可以注释这一行看看有什么变化。

鼠标停留在函数上可以查看文档。也可以前往安卓开发网站的API reference搜索。

之后的setContent函数则是我们主要关心的部分。注意到在Compose里,UI布局是用函数表示的。 代码用Greeting函数描述了一个UI:空空的页面,其中有一行字。 Greeting外层用Scaffold包裹,这是官方推荐的应用布局(毕竟叫“脚手架”),Greeting属于它的“内容”这部分。 Scaffold外层又有MyDroidTheme包裹,这是整个应用的主题,定义在ui/theme/包的Theme.kt中。

Android Studio左侧文件结构与文件夹结构并不等同,这是由上方的App项目结构显示下拉框决定的。 依次展开kotlin+java、包名、ui.theme即可看到Color.kt、Theme.kt、Type.kt。

Greeting函数的定义就在下方。它有@Compose注解,表示自己是一个Compose组件。 接收两个参数,要显示的文本,以及用于修改样式的修改器,函数内部还调用了Material 3的组件Text。 通过函数的层层调用,Compose用组合的方式搭建了表示页面的组件树,根组件为Scaffold,它的content参数为Greeting,而Greeting仅包含一个Text

你可能会认为嵌套太深影响性能,实则不然,这正是Compose的运作方式。 Compose不使用继承现有组件做修改来创造新组件,而是建议把需要的功能自由组合,由此灵活的创造界面。

更下方的xxxPreview是一个预览组件,让你不必运行软件就能看到界面的样子,但软件功能早晚还是要运行查看的,本文中并未用到这一功能。

读者现在可以用数据线连接手机电脑,确保开发者模式中开启了USB调试,即可在Android Studio上方看见设备名称,点击运行或按Shift + F10即可运行项目。

在手机上,可以看见空白的页面左上角出现了Hello Android!,这就是现有的代码对应的界面啦。

组件介绍

实际上,由于组件用函数表示,你可以直接用Text代替Greeting达到一样的效果。 多使用一个函数是为了让代码更加清晰,把独立的一块拆成一个函数组件,以后也可以重复使用。

Scaffold是一个脚手架组件,把鼠标移到它上面,可以看到它的参数列表,其中大部分是有默认值的,不需要我们指定。 最后一个参数content参数为@Composable ((PaddingValues) -> Unit),即一个组件函数。 它就对应我们传入的Greeting吗?不,函数签名都不一致,怎么会呢。 实际上我们传入的是一个lambda表达式,参数命名为innerPadding,无返回值(即返回Unit),而这个传参的写法很有趣,它竟然在Scaffold参数括号的外面。 这是Kotlin语言的一个功能,当函数的最后一个参数为lambda表达式时,它可以被放到函数调用表达式的外面。不要与函数定义混淆噢。 我们在这个lambda中调用了Greeting函数,从而成功将这个组件塞进了Scaffold的内容部分。

来看看这几个参数,首先是脚手架内容lambda的参数innerPadding,它包含四个内边距,上对应状态栏的高度,下对应导航栏的高度(有侧滑返回的全面屏手机应该没有这个)。 通过把这几个边距作为参数供内容使用,我们就不需要手动拉开边距了。 这里的使用方法是把innerPadding使用padding方法施加给Modifier,创造出一个新的Modifier示例,传递给Text的对应参数。 最终Text应用这个修改器,文本就会与顶部有一个状态栏那么高的上边距了。

Scaffold还使用了一个Modifier.fillMaxSize()修改器,占满父元素剩余空间,不过它自己就是根元素,因此占满手机屏幕空间。

自由组合

接下来你想写什么界面呢?可以在安卓官方对Compose的介绍文档中翻看各种组件,用他们组合成想要的界面。 为了教学和熟悉Compose,我们还是示范一种很常用的页面,一个推文列表吧。

从官方文档的推荐看,列表应该使用ColumnLazyColumn搭建。 前者适合普通的列式排列,适合仅仅想把几个组件竖着放的场合,而后者针对大量列表项做有优化,适合展示推文列表等。

想象一下推特、QQ空间的样子,你应该可以将它们拆分成几个组件。 每条推文的结构都是一致的,包含头像、呢称、操作、文本内容、点赞、时间以及数量不定的图片、评论。 这里我们简化成几类:仅供展示的固定项呢称内容,可选的图片,可交互的点赞图标,此外还可以设置长按或点击的操作。 想好了这些,再搜索文档,你应该可以写出这样表示一条推文的极简PostCard组件(图像以后再学习吧):

@Composable  
fun PostCard(name: String, content: String, liked: Boolean, comments: List<Pair<String, String>> = emptyList()) {  
    Text(name)  
    Text(content)  
    Icon(Icons.Default.ThumbUp, null)  
    comments.forEach {  
        Text(it.first)  
        Text(it.second)  
    }  
}

这里最高级的特性也只是ListPair类型、默认参数与lambda表达式。其中lambda的参数没有显式写名字,而是直接用it访问了。

想想它们的对齐方式与间距(就以QQ动态为参考),我们可以用更多组件与Modifier美化它:

@Composable
fun PostCard(
    name: String,
    content: String,
    liked: Boolean,
    comments: List<Pair<String, String>> = emptyList()
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ) {
        Text(name, style = MaterialTheme.typography.bodyLarge)
        Text(content, style = MaterialTheme.typography.bodyMedium)
        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
            if (liked) {
                Icon(Icons.Default.ThumbUp, null)
            } else {
                Icon(Icons.Outlined.ThumbUp, null)
            }
        }
        comments.forEach {
            Row(modifier = Modifier.fillMaxWidth()) {
                Text(it.first + ":", style = MaterialTheme.typography.bodySmall)
                Text(it.second, style = MaterialTheme.typography.bodySmall)
            }
        }
    }
}

不妨先用它代替Greeting,用简单的Column作为列表,运行一次看看效果。

        setContent {
            ExampleTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(modifier = Modifier.padding(innerPadding)) {
                        PostCard(
                            "名字",
                            "这是一条测试内容。今天天气真好。",
                            false,
                            listOf(Pair("系统", "你说的对"), Pair("系统", "你说的好"))
                        )
                    }
                }
            }
        }

这里用到了几个新功能:

你可能觉得内容间缺少边界,所有文字背景好像都融合在一起。可以通过添加SurfaceCard、设置elevation参数和添加水平线解决。

如果你按住Ctrl键单击Card,你可以跳转到它在源码中的定义。原来它只是一个Surface加Column!