第一章 创建你的第一个Compose应用
Jetpack Compose是谷歌针对Android的声明式UI框架,它大大简化了UI的创建。但在进一步学习之前,我们知道,Jetpack Compose仅适用于Kotlin。这意味着我们接下来创建的工程都必须用Kotlin编程。所以在学习本专题之前,读者应该对Kotlin语法和函数式编程有基本的了解。后续我会陆续推出关于Kotlin的专题,欢迎一起学习交流。
本章包含以下三个主题:
- 第一个Compose程序:HelloWord
- 使用预览函数
- 运行 Compose 项目
(一)第一个Compose程序:HelloWord
接下来你会看到,在Jetpack Compose中,可组合函数是UI的基本元素,通过可组合函数我们可以构建复杂的UI界面。所以我们首先通过一个HelloWord程序来学习可组合函数。这个程序会有一个输入名称的按钮还有完成按钮,输入名字点击完成后,界面会出现一段问候文本。
根据需求分析,这个程序包含以下内容:
- 第一是一段欢迎文本
- 第二是一个输入框和一个完成按钮
- 第三是一段问候文本
接下来让我们马上开始吧!
1.一段欢迎文本
接下来我们编写我们的第一个Compose函数,一段欢迎文本。
MainActivity.kt
@Composable
fun Welcome() {
Text(text = stringResource(id = R.string.welcome),
style = MaterialTheme.typography.subtitle1)
}
strings.xml
<string name="welcome">欢迎</string>
我们可以通过@Composable注解来标识可组合函数。它们不需要有特定的返回类型,而是发出界面元素,从而被其他可组合函数调用。Composable表示函数/lambda表达式可作为组合的一部分,将应用程序数据转换为树或层次结构。
这个Welcome可组合函数里面包含一个Text()元素,他有两个参数,text参数引用了strings.xml文件的welcome文本,style参数调用了预置的Material主题的subtitle1。
接下来我们再创建一个@Composable函数,一段问候文本。看看与之前的Welcome函数有何不同?
MainActivity.kt
@Composable
fun Greeting(name: String) {
Text(
text = stringResource(id = R.string.hello,name),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1
)
}
这里的text参数我们使用了带参数name的文本,可以非常方便地替代文本中的变量。
strings.xml
<string name="hello">你好,%1$s.\n非常高兴见到你。</string>
上面的%1代表第一个参数,$s代表替代的文本。
2.包含输入框和完成按钮的一行
这个输入框和完成按钮在同一行,Row属于非常常见的三大基本布局(Row,Column,Box)之一。与其他的Composable函数一样,Row(){},我们可以向小括号()里面传入若干参数,及向大括号{}里面传入若干子元素来组成界面。
MainActivity.kt
@Composable
fun TextAndButton(name: MutableState<String>, nameEntered: MutableState<Boolean>) {
Row(modifier = Modifier.padding(top = 8.dp)) {
TextField(
value = name.value,
onValueChange = {
name.value = it
},
placeholder = {
Text(text = stringResource(id = R.string.hint))
},
modifier = Modifier
.alignByBaseline()
.weight(1.0F),
singleLine = true,
keyboardOptions = KeyboardOptions(
autoCorrect = false,
capitalization = KeyboardCapitalization.Words
),
keyboardActions = KeyboardActions(onAny = {
nameEntered.value = true
})
)
Button(modifier = Modifier
.alignByBaseline()
.padding(8.dp),
onClick = {
nameEntered.value = true
}) {
Text(text = stringResource(id = R.string.done))
}
}
}
strings.xml
<string name="hint">你的名字</string>
<string name="done">完成</string>
上面我们创建一行,使用Row函数,在这一行里面添加一个输入框TextField和一个按钮Button。
输入框TextField可以传入很多参数,(注意:我们使用了value = name.value这样的形式,这样可以不需要考虑参数的位置)但大部分是可选的。
TextAndButton函数要求传入两个函数,name和nameEntered。这两个参数使用了MutableState类型,MutableState对象携带的值是可变的。value 值的状态如有任何更改,系统会安排重组读取 value 的所有可组合函数,这就是可组合函数的状态与重组。至于为什么要在onValueChange和keyboardActions这两个地方修改参数的值,我将在后面进行说明。
Button函数我们使用alignByBaseline()使按钮和输入框基线对齐,使用padding设置按钮的内边距。
3.显示一段问候文本
我们使用Box()布局,当用户输入名字后,显示一段问候文本,否则显示输入框和按钮。
MainActivity.kt
@Composable
fun Hello(){
val name = remember { mutableStateOf("")}
val nameEntered = remember { mutableStateOf(false)}
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
){
if (nameEntered.value) {
Greeting(name = name.value)
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Welcome()
TextAndButton(name = name, nameEntered = nameEntered)
}
}
}
}
这里你可能注意到了remember和mutableStateOf,这两个关键字对可组合函数状态的创建和和控制非常重要。状态涉及到界面元素中的变量,回顾前面的Welcome函数:
@Composable
fun Welcome() {
Text(text = stringResource(id = R.string.welcome),
style = MaterialTheme.typography.subtitle1)
}
Welcome()可以说是无状态的,因为他重新编译的值始终保持不变。而Hello()是有状态的,因为它使用name和nameEntered变量,传递给TextAndButton(),并在那里进行修改,使得它不断变化。
前面提到的,为什么要在onValueChange和keyboardActions这两个地方修改参数的值?TextAndButton()在onValueChange的地方组件状态会发生改变,我们以参数的形式,由Hello()组件传递进来,使TextAndButton()由有状态变为无状态,方便不同的情况调用,这种模式称为状态提升。所以状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。
我们编写了一个Composable函数之后,需要确认UI编写是否正确并对细节进行微调,这时候就需要使用Compose的预览函数。
(二)使用预览函数
1.带参数的预览函数
使用Compose的预览函数,我们需要在Composable函数上再添加一个注解@Preview,如果我们在Greeting(name: String)上面添加@Preview,你会看到程序报错:
Composable functions with non-default parameters are not supported in Preview unless they are annotated with @PreviewParameter.
所以,我们应该怎么预览带参数的Composable函数呢?
最简单的方法,是给改函数外面再包上一层不带参数的Composable函数。
@Preview
@Composable
fun PreviewGreeting(){
Greeting(name = "Jetpack Compose")
}
这意味着我们每次都需要重新写一个多余的函数来达到预览的效果。如果我们带参数的可组合函数非常多,这样工作量就会非常大。
好在我们还有其他方法,例如,我们可以加一个函数参数的默认值。
@Preview
@Composable
fun Greeting(name: String = "Jetpack Compose") {
Text(
text = stringResource(id = R.string.hello,name),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1
)
}
这样就方便很多,但同样存在一个问题,如果我们这个可组合函数不需要,或者说不能给他设置默认值,那这种方法也不可行。
根据提示,使用@PreviewParameter,我们可以给可组合函数传递参数值只影响预览函数。这个方法有一点麻烦之处在于,我们需要编写一个新的类:
class HelloProvider: PreviewParameterProvider<String> {
override val values: Sequence<String>
get() = listOf("PreviewParameterProvider").asSequence()
}
这样,我们只需要在composable函数里面添加@PreviewParameter注解,这个类就会提供一个参数给预览函数。
@Preview
@Composable
fun Greeting(@PreviewParameter(HelloProvider::class)name: String) {
Text(
text = stringResource(id = R.string.hello,name),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1
)
}
对于带参数的预览函数,以上几种方法都可以使用,根据个人喜好和具体情况而定。此外,@Preview注解还可以通过设置一些参数,来修改预览界面的外观。
2.@Preview注解参数配置
我们可以为预览设置背景颜色,当然,首先要确认设置显示背景为true。
@Preview(showBackground = true, backgroundColor = 0xffff0000)
@Composable
fun DefaultPreview() {
Hello()
}
同理,预览的尺寸一般是自适应的,但是我们也可以为预览设置固定的宽高。
@Preview(widthDp = 100, heightDp = 100)
@Composable
fun DefaultPreview() {
Hello()
}
测试多国语言的时候,如果我们在string-zh-rCN里设置了翻译语言,就可以通过locale参数,设置预览显示的语言。
@Preview(locale = "zh-rCN")
@Composable
fun DefaultPreview() {
Hello()
}
如果想显示状态栏和动作栏,我们可以设置showSystemUi:
@Preview(showSystemUi = true)
@Composable
fun DefaultPreview() {
Hello()
}
3.分组预览
当代码中我们设置了多个预览函数时,我们可以选择这些预览函数在右侧预览面板以网格或者垂直的方式展示。
当我们代码中设置了非常多的预览函数,导致预览面板看起来十分混乱,这时我们可以设置在右侧预览面板上面进行分组预览。
新建一个预览分组:
@Preview(group = "group1")
@Composable
fun Welcome() {
Text(text = stringResource(id = R.string.welcome),
style = MaterialTheme.typography.subtitle1)
}
切换分组视图:
(三)运行Compose应用
如果我们想看看界面UI及一些交互操作在模拟器及真机上的效果,我们可以通过以下两个方式:
- 部署Composable函数
- 运行App
1.部署Composable函数
我们在预览面板的某个预览函数的预览界面的右上角,有一个预览按钮,点击即可部署到真机或模拟器上,这种方法比较适合调试单个Composable函数的时候。
这种方法会为我们自动创建运行配置,我们可以在Run/Debug Comfigurations里面进行修改。
2.在Activity上使用Composable函数
普通情况下,我们新建的工程,在AndroidManifest.xml项目里面就将MainActivity设置为启动界面。并在MainActivity里设置它对应的布局。同样的,使用Compose时,我们也需要在Activity里面绑定Compose表示的界面。
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Welcome()
}
}
}
我们通过setContent{ }就可以简洁明了地设置对应的布局。与之前的setContentView()相比,使用Jetpack Compose,不需要维护对UI组件树或其单个元素的引用。这点我们会在后面详细介绍。
3.项目的配置
Jetpack Compose依赖Kotlin编写,这意味着我们的应用程序项目必须配置为Kotlin工程,但这并不意味着我们完全不能使用Java。事实上,只要我们的可组合函数是用Kotlin编写的,就可以在项目中轻松地混合Kotlin和Java,同时也可以混合使用传统视图和可组合视图。关于这个互操作性API主题我们将在后面详细介绍。
在创建项目的时候,我们只需要选择Empty Compose Activity,AndroidStudio就会为我们做好一个Compose项目的所有配置。
这包括在项目级别的build.gradle里面对Kotlin的引用和配置,API版本不低于21,及在应用级别的build.gradle里面引入compose相关的依赖库。
4.点击运行按钮
运行我们的App,首先确定我们运行的app(下图的app处)是否已选择,并确认我们要运行的设备(下图的Pixel XL API 30处)是否已选择,然后点击绿色播放按钮,即可成功运行我们的应用。
至此,我们开发的第一HelloWorld应用就顺利完成了!
(四)总结回顾
1.总结
这一章,我们学习了如何编写我们的第一个Compose程序,并成功运行到设备上。同时,在编写代码的过程中,我们了解到了如何使用@Composable注解编写一个可组合函数,及如何使用@Preview注解在预览面板预览可组合函数。另外,对可组合函数的状态与重组及状态提升等概念也有了基本的了解。
2.回顾
关键术语
@Composable
带参数的字符串
三大布局基础布局(Row/Column/Box)
remember
mutableStateOf
可组合函数的状态与重组
状态提升
@Preview
@PreviewParameter
项目的部署与运行