第一章 創(chuàng)建你的第一個(gè)Compose應(yīng)用
Jetpack Compose是谷歌針對(duì)Android的聲明式UI框架,它大大簡(jiǎn)化了UI的創(chuàng)建。但在進(jìn)一步學(xué)習(xí)之前,我們知道,Jetpack Compose僅適用于Kotlin。這意味著我們接下來(lái)創(chuàng)建的工程都必須用Kotlin編程。所以在學(xué)習(xí)本專題之前,讀者應(yīng)該對(duì)Kotlin語(yǔ)法和函數(shù)式編程有基本的了解。后續(xù)我會(huì)陸續(xù)推出關(guān)于Kotlin的專題,歡迎一起學(xué)習(xí)交流。
本章包含以下三個(gè)主題:
- 第一個(gè)Compose程序:HelloWord
- 使用預(yù)覽函數(shù)
- 運(yùn)行 Compose 項(xiàng)目
(一)第一個(gè)Compose程序:HelloWord
接下來(lái)你會(huì)看到,在Jetpack Compose中,可組合函數(shù)是UI的基本元素,通過(guò)可組合函數(shù)我們可以構(gòu)建復(fù)雜的UI界面。所以我們首先通過(guò)一個(gè)HelloWord程序來(lái)學(xué)習(xí)可組合函數(shù)。這個(gè)程序會(huì)有一個(gè)輸入名稱的按鈕還有完成按鈕,輸入名字點(diǎn)擊完成后,界面會(huì)出現(xiàn)一段問(wèn)候文本。
根據(jù)需求分析,這個(gè)程序包含以下內(nèi)容:
- 第一是一段歡迎文本
- 第二是一個(gè)輸入框和一個(gè)完成按鈕
- 第三是一段問(wèn)候文本

接下來(lái)讓我們馬上開(kāi)始吧!
1.一段歡迎文本
接下來(lái)我們編寫(xiě)我們的第一個(gè)Compose函數(shù),一段歡迎文本。
MainActivity.kt
@Composable
fun Welcome() {
Text(text = stringResource(id = R.string.welcome),
style = MaterialTheme.typography.subtitle1)
}
strings.xml
<string name="welcome">歡迎</string>
我們可以通過(guò)@Composable注解來(lái)標(biāo)識(shí)可組合函數(shù)。它們不需要有特定的返回類型,而是發(fā)出界面元素,從而被其他可組合函數(shù)調(diào)用。Composable表示函數(shù)/lambda表達(dá)式可作為組合的一部分,將應(yīng)用程序數(shù)據(jù)轉(zhuǎn)換為樹(shù)或?qū)哟谓Y(jié)構(gòu)。
這個(gè)Welcome可組合函數(shù)里面包含一個(gè)Text()元素,他有兩個(gè)參數(shù),text參數(shù)引用了strings.xml文件的welcome文本,style參數(shù)調(diào)用了預(yù)置的Material主題的subtitle1。
接下來(lái)我們?cè)賱?chuàng)建一個(gè)@Composable函數(shù),一段問(wèn)候文本??纯磁c之前的Welcome函數(shù)有何不同?
MainActivity.kt
@Composable
fun Greeting(name: String) {
Text(
text = stringResource(id = R.string.hello,name),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1
)
}
這里的text參數(shù)我們使用了帶參數(shù)name的文本,可以非常方便地替代文本中的變量。
strings.xml
<string name="hello">你好,%1$s.\n非常高興見(jiàn)到你。</string>
上面的%1代表第一個(gè)參數(shù),$s代表替代的文本。
2.包含輸入框和完成按鈕的一行
這個(gè)輸入框和完成按鈕在同一行,Row屬于非常常見(jiàn)的三大基本布局(Row,Column,Box)之一。與其他的Composable函數(shù)一樣,Row(){},我們可以向小括號(hào)()里面?zhèn)魅肴舾蓞?shù),及向大括號(hào){}里面?zhèn)魅肴舾勺釉貋?lái)組成界面。
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>
上面我們創(chuàng)建一行,使用Row函數(shù),在這一行里面添加一個(gè)輸入框TextField和一個(gè)按鈕Button。
輸入框TextField可以傳入很多參數(shù),(注意:我們使用了value = name.value這樣的形式,這樣可以不需要考慮參數(shù)的位置)但大部分是可選的。
TextAndButton函數(shù)要求傳入兩個(gè)函數(shù),name和nameEntered。這兩個(gè)參數(shù)使用了MutableState類型,MutableState對(duì)象攜帶的值是可變的。value 值的狀態(tài)如有任何更改,系統(tǒng)會(huì)安排重組讀取 value 的所有可組合函數(shù),這就是可組合函數(shù)的狀態(tài)與重組。至于為什么要在onValueChange和keyboardActions這兩個(gè)地方修改參數(shù)的值,我將在后面進(jìn)行說(shuō)明。
Button函數(shù)我們使用alignByBaseline()使按鈕和輸入框基線對(duì)齊,使用padding設(shè)置按鈕的內(nèi)邊距。
3.顯示一段問(wèn)候文本
我們使用Box()布局,當(dāng)用戶輸入名字后,顯示一段問(wèn)候文本,否則顯示輸入框和按鈕。
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,這兩個(gè)關(guān)鍵字對(duì)可組合函數(shù)狀態(tài)的創(chuàng)建和和控制非常重要。狀態(tài)涉及到界面元素中的變量,回顧前面的Welcome函數(shù):
@Composable
fun Welcome() {
Text(text = stringResource(id = R.string.welcome),
style = MaterialTheme.typography.subtitle1)
}
Welcome()可以說(shuō)是無(wú)狀態(tài)的,因?yàn)樗匦戮幾g的值始終保持不變。而Hello()是有狀態(tài)的,因?yàn)樗褂胣ame和nameEntered變量,傳遞給TextAndButton(),并在那里進(jìn)行修改,使得它不斷變化。
前面提到的,為什么要在onValueChange和keyboardActions這兩個(gè)地方修改參數(shù)的值?TextAndButton()在onValueChange的地方組件狀態(tài)會(huì)發(fā)生改變,我們以參數(shù)的形式,由Hello()組件傳遞進(jìn)來(lái),使TextAndButton()由有狀態(tài)變?yōu)闊o(wú)狀態(tài),方便不同的情況調(diào)用,這種模式稱為狀態(tài)提升。所以狀態(tài)提升是一種將狀態(tài)移至可組合項(xiàng)的調(diào)用方以使可組合項(xiàng)無(wú)狀態(tài)的模式。
我們編寫(xiě)了一個(gè)Composable函數(shù)之后,需要確認(rèn)UI編寫(xiě)是否正確并對(duì)細(xì)節(jié)進(jìn)行微調(diào),這時(shí)候就需要使用Compose的預(yù)覽函數(shù)。
(二)使用預(yù)覽函數(shù)
1.帶參數(shù)的預(yù)覽函數(shù)
使用Compose的預(yù)覽函數(shù),我們需要在Composable函數(shù)上再添加一個(gè)注解@Preview,如果我們?cè)贕reeting(name: String)上面添加@Preview,你會(huì)看到程序報(bào)錯(cuò):
Composable functions with non-default parameters are not supported in Preview unless they are annotated with @PreviewParameter.
所以,我們應(yīng)該怎么預(yù)覽帶參數(shù)的Composable函數(shù)呢?
最簡(jiǎn)單的方法,是給改函數(shù)外面再包上一層不帶參數(shù)的Composable函數(shù)。
@Preview
@Composable
fun PreviewGreeting(){
Greeting(name = "Jetpack Compose")
}
這意味著我們每次都需要重新寫(xiě)一個(gè)多余的函數(shù)來(lái)達(dá)到預(yù)覽的效果。如果我們帶參數(shù)的可組合函數(shù)非常多,這樣工作量就會(huì)非常大。
好在我們還有其他方法,例如,我們可以加一個(gè)函數(shù)參數(shù)的默認(rèn)值。
@Preview
@Composable
fun Greeting(name: String = "Jetpack Compose") {
Text(
text = stringResource(id = R.string.hello,name),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1
)
}
這樣就方便很多,但同樣存在一個(gè)問(wèn)題,如果我們這個(gè)可組合函數(shù)不需要,或者說(shuō)不能給他設(shè)置默認(rèn)值,那這種方法也不可行。
根據(jù)提示,使用@PreviewParameter,我們可以給可組合函數(shù)傳遞參數(shù)值只影響預(yù)覽函數(shù)。這個(gè)方法有一點(diǎn)麻煩之處在于,我們需要編寫(xiě)一個(gè)新的類:
class HelloProvider: PreviewParameterProvider<String> {
override val values: Sequence<String>
get() = listOf("PreviewParameterProvider").asSequence()
}
這樣,我們只需要在composable函數(shù)里面添加@PreviewParameter注解,這個(gè)類就會(huì)提供一個(gè)參數(shù)給預(yù)覽函數(shù)。
@Preview
@Composable
fun Greeting(@PreviewParameter(HelloProvider::class)name: String) {
Text(
text = stringResource(id = R.string.hello,name),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.subtitle1
)
}
對(duì)于帶參數(shù)的預(yù)覽函數(shù),以上幾種方法都可以使用,根據(jù)個(gè)人喜好和具體情況而定。此外,@Preview注解還可以通過(guò)設(shè)置一些參數(shù),來(lái)修改預(yù)覽界面的外觀。
2.@Preview注解參數(shù)配置
我們可以為預(yù)覽設(shè)置背景顏色,當(dāng)然,首先要確認(rèn)設(shè)置顯示背景為true。
@Preview(showBackground = true, backgroundColor = 0xffff0000)
@Composable
fun DefaultPreview() {
Hello()
}

同理,預(yù)覽的尺寸一般是自適應(yīng)的,但是我們也可以為預(yù)覽設(shè)置固定的寬高。
@Preview(widthDp = 100, heightDp = 100)
@Composable
fun DefaultPreview() {
Hello()
}
測(cè)試多國(guó)語(yǔ)言的時(shí)候,如果我們?cè)趕tring-zh-rCN里設(shè)置了翻譯語(yǔ)言,就可以通過(guò)locale參數(shù),設(shè)置預(yù)覽顯示的語(yǔ)言。
@Preview(locale = "zh-rCN")
@Composable
fun DefaultPreview() {
Hello()
}
如果想顯示狀態(tài)欄和動(dòng)作欄,我們可以設(shè)置showSystemUi:
@Preview(showSystemUi = true)
@Composable
fun DefaultPreview() {
Hello()
}
3.分組預(yù)覽
當(dāng)代碼中我們?cè)O(shè)置了多個(gè)預(yù)覽函數(shù)時(shí),我們可以選擇這些預(yù)覽函數(shù)在右側(cè)預(yù)覽面板以網(wǎng)格或者垂直的方式展示。

當(dāng)我們代碼中設(shè)置了非常多的預(yù)覽函數(shù),導(dǎo)致預(yù)覽面板看起來(lái)十分混亂,這時(shí)我們可以設(shè)置在右側(cè)預(yù)覽面板上面進(jìn)行分組預(yù)覽。
新建一個(gè)預(yù)覽分組:
@Preview(group = "group1")
@Composable
fun Welcome() {
Text(text = stringResource(id = R.string.welcome),
style = MaterialTheme.typography.subtitle1)
}
切換分組視圖:

(三)運(yùn)行Compose應(yīng)用
如果我們想看看界面UI及一些交互操作在模擬器及真機(jī)上的效果,我們可以通過(guò)以下兩個(gè)方式:
- 部署Composable函數(shù)
- 運(yùn)行App
1.部署Composable函數(shù)
我們?cè)陬A(yù)覽面板的某個(gè)預(yù)覽函數(shù)的預(yù)覽界面的右上角,有一個(gè)預(yù)覽按鈕,點(diǎn)擊即可部署到真機(jī)或模擬器上,這種方法比較適合調(diào)試單個(gè)Composable函數(shù)的時(shí)候。

這種方法會(huì)為我們自動(dòng)創(chuàng)建運(yùn)行配置,我們可以在Run/Debug Comfigurations里面進(jìn)行修改。
2.在Activity上使用Composable函數(shù)
普通情況下,我們新建的工程,在AndroidManifest.xml項(xiàng)目里面就將MainActivity設(shè)置為啟動(dòng)界面。并在MainActivity里設(shè)置它對(duì)應(yīng)的布局。同樣的,使用Compose時(shí),我們也需要在Activity里面綁定Compose表示的界面。
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Welcome()
}
}
}
我們通過(guò)setContent{ }就可以簡(jiǎn)潔明了地設(shè)置對(duì)應(yīng)的布局。與之前的setContentView()相比,使用Jetpack Compose,不需要維護(hù)對(duì)UI組件樹(shù)或其單個(gè)元素的引用。這點(diǎn)我們會(huì)在后面詳細(xì)介紹。
3.項(xiàng)目的配置
Jetpack Compose依賴Kotlin編寫(xiě),這意味著我們的應(yīng)用程序項(xiàng)目必須配置為Kotlin工程,但這并不意味著我們完全不能使用Java。事實(shí)上,只要我們的可組合函數(shù)是用Kotlin編寫(xiě)的,就可以在項(xiàng)目中輕松地混合Kotlin和Java,同時(shí)也可以混合使用傳統(tǒng)視圖和可組合視圖。關(guān)于這個(gè)互操作性API主題我們將在后面詳細(xì)介紹。
在創(chuàng)建項(xiàng)目的時(shí)候,我們只需要選擇Empty Compose Activity,AndroidStudio就會(huì)為我們做好一個(gè)Compose項(xiàng)目的所有配置。

這包括在項(xiàng)目級(jí)別的build.gradle里面對(duì)Kotlin的引用和配置,API版本不低于21,及在應(yīng)用級(jí)別的build.gradle里面引入compose相關(guān)的依賴庫(kù)。
4.點(diǎn)擊運(yùn)行按鈕
運(yùn)行我們的App,首先確定我們運(yùn)行的app(下圖的app處)是否已選擇,并確認(rèn)我們要運(yùn)行的設(shè)備(下圖的Pixel XL API 30處)是否已選擇,然后點(diǎn)擊綠色播放按鈕,即可成功運(yùn)行我們的應(yīng)用。

至此,我們開(kāi)發(fā)的第一HelloWorld應(yīng)用就順利完成了!
(四)總結(jié)回顧
1.總結(jié)
這一章,我們學(xué)習(xí)了如何編寫(xiě)我們的第一個(gè)Compose程序,并成功運(yùn)行到設(shè)備上。同時(shí),在編寫(xiě)代碼的過(guò)程中,我們了解到了如何使用@Composable注解編寫(xiě)一個(gè)可組合函數(shù),及如何使用@Preview注解在預(yù)覽面板預(yù)覽可組合函數(shù)。另外,對(duì)可組合函數(shù)的狀態(tài)與重組及狀態(tài)提升等概念也有了基本的了解。
2.回顧
關(guān)鍵術(shù)語(yǔ)
@Composable
帶參數(shù)的字符串
三大布局基礎(chǔ)布局(Row/Column/Box)
remember
mutableStateOf
可組合函數(shù)的狀態(tài)與重組
狀態(tài)提升
@Preview
@PreviewParameter
項(xiàng)目的部署與運(yùn)行