Compose基础-创建你的第一个Compose应用

第一章 创建你的第一个Compose应用

Jetpack Compose是谷歌针对Android的声明式UI框架,它大大简化了UI的创建。但在进一步学习之前,我们知道,Jetpack Compose仅适用于Kotlin。这意味着我们接下来创建的工程都必须用Kotlin编程。所以在学习本专题之前,读者应该对Kotlin语法和函数式编程有基本的了解。后续我会陆续推出关于Kotlin的专题,欢迎一起学习交流。

本章包含以下三个主题:
  • 第一个Compose程序:HelloWord
  • 使用预览函数
  • 运行 Compose 项目

(一)第一个Compose程序:HelloWord

接下来你会看到,在Jetpack Compose中,可组合函数是UI的基本元素,通过可组合函数我们可以构建复杂的UI界面。所以我们首先通过一个HelloWord程序来学习可组合函数。这个程序会有一个输入名称的按钮还有完成按钮,输入名字点击完成后,界面会出现一段问候文本。
根据需求分析,这个程序包含以下内容:

  • 第一是一段欢迎文本
  • 第二是一个输入框和一个完成按钮
  • 第三是一段问候文本
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)
            }
        }
    }
}

这里你可能注意到了remembermutableStateOf,这两个关键字对可组合函数状态的创建和和控制非常重要。状态涉及到界面元素中的变量,回顾前面的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()
}
HelloWord红色背景

同理,预览的尺寸一般是自适应的,但是我们也可以为预览设置固定的宽高。

@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项目的所有配置。

新建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
项目的部署与运行

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

推荐阅读更多精彩内容