簡(jiǎn)介
Jetpack Compose 是 Google 官方 2019 年推出的UI框架种吸,它可簡(jiǎn)化并加快 Android 的 UI 開(kāi)發(fā)工作还栓。使用更少的代碼钮莲、強(qiáng)大的工具和直觀的 Kotlin API忿磅,快速構(gòu)建 App 的 UI另患。2021年馬上就將迎來(lái) Compose 的正式版囊陡,是時(shí)候來(lái)了解一下這個(gè)官方強(qiáng)推的芳绩,布局機(jī)制、渲染機(jī)制撞反、具體寫(xiě)法等可以說(shuō)是全新的UI框架了妥色。
先來(lái)看一段簡(jiǎn)單的 Compose 代碼:
Column {
Text("Hello world")
Image()
}
OK這就是一個(gè)完整的UI界面了,對(duì)比原來(lái)定義在 xml 文件中的方式有著天壤之別遏片,展現(xiàn)一個(gè)UI不再是去創(chuàng)建一個(gè) TextView 之類(lèi)的控件嘹害,而是變成了一次函數(shù)調(diào)用。雖然 Text 以大寫(xiě)開(kāi)頭吮便,但它其實(shí)就是一個(gè)普通函數(shù)笔呀,嚴(yán)格說(shuō)是個(gè)帶 @Composable
注解的 Compose 函數(shù):
@Composable
fun Text(...) {
...
}
來(lái)看一段完善一些的 Compose 代碼:
@Composable
fun NewsStory() {
MaterialTheme {
val typography = MaterialTheme.typography
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(R.drawable.header),
contentDescription = null,
modifier = Modifier
.height(180.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(4.dp)),
contentScale = ContentScale.Crop
)
Spacer(Modifier.height(16.dp))
Text(
"A day wandering through the sandhills " +
"in Shark Fin Cove, and a few of the " +
"sights I saw",
style = typography.h6,
maxLines = 2,
overflow = TextOverflow.Ellipsis)
Text("Davenport, California", style = typography.body2)
Text("December 2018", style = typography.body2)
}
}
}
以 Column、Row 代替 LinearLayout 等布局髓需,以 Text许师、Image 等代替 TextView、ImageView 等控件僚匆,以 Modifier 等用作細(xì)節(jié)和修飾微渠,所以其實(shí) Compose 就是這樣由多個(gè)函數(shù)調(diào)用組合起來(lái),形成一個(gè)完整的 UI 界面。
Compose 改變了原有的基于 xml 和 View 的體系,純?cè)诖a中實(shí)現(xiàn)頁(yè)面UI蚕苇,那么它比起老的方式有什么優(yōu)勢(shì)呢?
Compose 的特點(diǎn)
Jetpack Compose is Android’s modern toolkit for building native UI.
這是官方對(duì) Compose 的定義云芦,比起舊有體系,Compose 更加 “現(xiàn)代”攻臀。
現(xiàn)有的 Android 視圖體系從 2010年以來(lái)沒(méi)有發(fā)生太大變化焕数,10年間無(wú)論從硬件規(guī)格還是APP復(fù)雜度都發(fā)生了極大變化纱昧,這套已經(jīng)跑了10年的技術(shù)體系也已經(jīng)顯得有些落伍刨啸。
聲明式 vs 命令式
說(shuō)起 Compose 最大的特點(diǎn),就是它是聲明式的识脆,而現(xiàn)有體系是命令式的设联。
-
命令式:現(xiàn)有視圖體系要先將UI定義在 xml 文件中善已,當(dāng)需要刷新時(shí),需要在代碼中先
findViewById
獲取控件的引用离例,再下達(dá)如setText
换团、setVisibility
等命令,主動(dòng)要求更新?tīng)顟B(tài)宫蛆、刷新UI艘包。隨著界面越來(lái)越復(fù)雜,控件越來(lái)越多耀盗,各控件狀態(tài)難以保持同步想虎,UI顯示不一致的Bug頻發(fā)。我們的很多精力花費(fèi)在了如何能準(zhǔn)確且不遺漏地更新所有該更新的控件上叛拷。
-
聲明式:聲明式UI以一個(gè)“純函數(shù)”的方式運(yùn)行舌厨,當(dāng) State 變化時(shí)函數(shù)根據(jù)傳入?yún)?shù)重新執(zhí)行刷新UI。
Compose 會(huì)對(duì)界面中用到的數(shù)據(jù)自動(dòng)進(jìn)行訂閱——不管是字符串還是圖像還是別的什么忿薇,Compose 全部能夠自動(dòng)訂閱——這樣當(dāng)數(shù)據(jù)改變的時(shí)候裙椭,Compose 會(huì)直接把新的數(shù)據(jù)更新到界面。
var text by mutableStateOf("Hello")
個(gè)人理解就是署浩,只需要把界面給提前“聲明”出來(lái)揉燃,先定義好在各種 state 時(shí) UI 應(yīng)該是個(gè)什么樣子,當(dāng)數(shù)據(jù)產(chǎn)生變化時(shí)筋栋,就不再需要去主動(dòng)下達(dá)
setVisibility
等各種命令你雌,界面會(huì)自動(dòng)更新。現(xiàn)有的
Data Binding
其實(shí)就是聲明式的二汛,但它通過(guò)數(shù)據(jù)更新的只能是界面元素的值婿崭,而 Compose 可以更新界面中的任何內(nèi)容,包括界面的結(jié)構(gòu)肴颊。比如以下根據(jù)數(shù)據(jù)變化整個(gè)UI結(jié)構(gòu)氓栈,
Data Binding
就無(wú)法做到:@Composable fun MessageList(messages: List<String>) { Column { if (message.size == 0) { Text("No messages") } else { message.forEach { message -> Text(text=messag) } } } }
高性能的重組(重繪)
在上面的例子里,當(dāng) message 發(fā)生變化時(shí)婿着,MessageList 重新執(zhí)行授瘦,這個(gè)過(guò)程叫重組(recomposition)。Composee 的 UI 正是通過(guò)不斷重組來(lái)實(shí)現(xiàn)刷新竟宋。
但如果數(shù)據(jù)變化時(shí)會(huì)觸發(fā)重組提完,大面積的重組是否會(huì)影響性能呢?
Compose 會(huì)通過(guò)在 Gap Buffer 這樣的線性結(jié)構(gòu)上進(jìn)行 diff 實(shí)現(xiàn)局部刷新丘侠。 Gap Buffer 可以理解為一個(gè)樹(shù)形結(jié)構(gòu)經(jīng) DFS 處理后的數(shù)組徒欣,數(shù)組單元通過(guò) key 標(biāo)記其在樹(shù)上的位置信息。Compose 在編譯期為 Composable 生成帶有位置信息的 key蜗字,存入到 Gap Buffer 數(shù)組的對(duì)應(yīng)位置打肝。運(yùn)行時(shí)可以根據(jù) key 來(lái)識(shí)別 Composable 節(jié)點(diǎn)是否發(fā)生了位置變化脂新,以決定是否參與重組。同時(shí)粗梭,Gap Buffer 還會(huì)記錄 Composable 對(duì)象關(guān)聯(lián)的狀態(tài)(State 或 Parameters)争便,僅僅當(dāng)關(guān)聯(lián)狀態(tài)變化時(shí),Composable 才會(huì)參與重組断医,函數(shù)才會(huì)重新執(zhí)行滞乙。
布局層級(jí)嵌套
做 Android 開(kāi)發(fā)的都知道一個(gè)規(guī)矩:布局文件的界面層級(jí)要盡量地少,因?yàn)閷蛹?jí)的增加會(huì)大幅拖慢界面的加載鉴嗤。這種拖慢的主要原因就在于各種 Layout 的重復(fù)測(cè)量酷宵。雖然重復(fù)測(cè)量對(duì)于布局過(guò)程是必不可少的,但這也確實(shí)讓界面層級(jí)的數(shù)量對(duì)加載時(shí)間的影響變成了指數(shù)級(jí)躬窜。
而 Compose 是不怕層級(jí)嵌套的浇垦,因?yàn)樗鼜母瓷辖鉀Q了這種問(wèn)題。它解決的方式也非常巧妙而簡(jiǎn)單——它不許重復(fù)測(cè)量荣挨。
Compose 通過(guò)一種叫做 Intrinsic Measurement(固有特性測(cè)量)的機(jī)制男韧,避免了隨著層級(jí)增多,重復(fù)測(cè)量導(dǎo)致繪制時(shí)間指數(shù)式增加的性能陷阱默垄,也就是說(shuō)此虑,使用 Compose 時(shí)瘋狂嵌套,和把所有組件寫(xiě)在同一層級(jí)里口锭,性能上是一樣的朦前!這是比起原體系的一大進(jìn)步。
配合其他 Jetpack 組件
@Composable
fun ConversationScreen() {
val viewModel: ConversatioinViewModel = viewModel()
val message by viewModel.messages.observeAsState()
MessageLit(messages)
}
@Composable
fun MessageList(message: List<String>){
...
}
Compose 可以配合現(xiàn)有 Jetpack 組件的使用鹃操,例如 ViewModel韭寸、LiveData 等,對(duì)于一個(gè)標(biāo)準(zhǔn)的 Jetpack MVVM項(xiàng)目荆隘,將很容易將 UI 部分替換成 Compose恩伺。
Composalbe 中調(diào)用 viewModel()
可以獲取當(dāng)前 Context 的 ViewModel, observeAsState()
將 LiveData 轉(zhuǎn)換為 Compose State 并建立綁定椰拒。當(dāng) LiveData 變化時(shí)晶渠,ConversationScreen 會(huì)發(fā)生重組,內(nèi)部的 MessageLit 燃观、MessageItem 由于依賴了參數(shù) messages褒脯,都會(huì)參與重組。
功能完備的UI系統(tǒng)
Compose目前的 UI 系統(tǒng)功能完備缆毁,可以完全覆蓋 Android 現(xiàn)有視圖系統(tǒng)的所有能力番川。
各種UI組件:所有常見(jiàn)的UI組件在 Compose 中都能找到對(duì)應(yīng)實(shí)現(xiàn),甚至Card、Fab爽彤、AppBar等 Material Designe 的控件也一應(yīng)俱全、開(kāi)箱即用 缚陷。
-
列表 List:Compose 的列表非常簡(jiǎn)單适篙,無(wú)需再寫(xiě)煩人的 Adapter。
@Composable fun MessageList(list: List<Message>) { Column { LazyList { // this :LazyListScope items(list) { item -> when(item.type) { Unread -> UnreadItem(message) Readed -> ReadedItem(message) } } } } }
-
布局 Layout:Compose 提供了多種容器類(lèi)Composalbe箫爷,可以對(duì)子組件進(jìn)行布局嚷节,簡(jiǎn)單易用且功能強(qiáng)大。
- Row ≈ Horizontal LinearLayout
- Column ≈ Vertical LinearLayout
- Box ≈ FragmeLayout
-
自定義布局:通過(guò)簡(jiǎn)單的函數(shù)調(diào)用完成 measure 和 layout 過(guò)程虎锚。
Layout( content = content, modifier = modifier ) { measurables, constraints -> // Measure the composables val placeables = measurable.measure(constraints) // Layout the comopsables layout(width, height) { placeables.forEach { placeable -> placeable.place(x, y) } } }
Modifier 操作符:Compose 通過(guò)一系列鏈?zhǔn)秸{(diào)用的 Modifier 操作符來(lái)裝飾 Composable 的外觀硫痰。操作符種類(lèi)繁多,例如 size窜护、backgrounds效斑、padding 的設(shè)置以及 click 事件的添加等。
-
動(dòng)畫(huà) Animatioin:Compose 動(dòng)畫(huà)也是基于 State 驅(qū)動(dòng)不斷重組完成的柱徙。
@Composable fun AnimateAsStateDemo() { var isHighLight by remember { mutableStateOf(false) } val color by animateColorAsState ( if (isHighLight) Red else Blue, ) val size by animateDpAsState ( if (isHighLight) LargeSize else SizeSize, ) Box(Modifier.size(size).background(color)) }
開(kāi)發(fā)中預(yù)覽
目前的基于 xml 的預(yù)覽效果很雞肋缓屠,導(dǎo)致很多開(kāi)發(fā)者都習(xí)慣于實(shí)機(jī)運(yùn)行查看UI。Compose 預(yù)覽機(jī)制可以做到與真機(jī)無(wú)異护侮,真正的所見(jiàn)所即得敌完。
預(yù)覽時(shí)只需創(chuàng)建一個(gè)無(wú)參的 Composalbe,并添加 @Preview 注解即可羊初。
@Preview
@Composable
fun PreviewGreeting() {
Greeting("Android")
}
與現(xiàn)有體系良好的互操作性
Compose 能夠與現(xiàn)有 View 體系能一起使用滨溉,比如在現(xiàn)有布局中使用 Compose,或在 Compose 布局中使用舊視圖體系长赞。所以遷移到 Compose 很方便晦攒,可以為一個(gè)已有項(xiàng)目先引入 Compose,再逐漸切換得哆,不要求一次性將舊UI全替換為新的勤家,有很大的緩沖空間。
實(shí)踐
現(xiàn)在來(lái)嘗試動(dòng)手寫(xiě)一個(gè)簡(jiǎn)單的 Compose 界面柳恐。
配置 Kotlin
plugins {
id("org.jetbrains.kotlin.android") version "1.4.32"
}
配置 Gradle
android {
defaultConfig {
...
minSdkVersion(21)
}
buildFeatures {
// Enables Jetpack Compose for this module
compose = true
}
...
// Set both the Java and Kotlin compilers to target Java 8.
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
useIR = true
}
composeOptions {
kotlinCompilerVersion = "1.4.32"
kotlinCompilerExtensionVersion = "1.0.0-beta07"
}
}
添加 Jetpack Compose 工具包依賴項(xiàng)
dependencies {
implementation("androidx.compose.ui:ui:1.0.0-beta07")
// Tooling support (Previews, etc.)
implementation("androidx.compose.ui:ui-tooling:1.0.0-beta07")
// Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
implementation("androidx.compose.foundation:foundation:1.0.0-beta07")
// Material Design
implementation("androidx.compose.material:material:1.0.0-beta07")
// Material design icons
implementation("androidx.compose.material:material-icons-core:1.0.0-beta07")
implementation("androidx.compose.material:material-icons-extended:1.0.0-beta07")
// Integration with observables
implementation("androidx.compose.runtime:runtime-livedata:1.0.0-beta07")
implementation("androidx.compose.runtime:runtime-rxjava2:1.0.0-beta07")
// UI Tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0-beta07")
}
用 Compose 來(lái)重新寫(xiě)一下 賬號(hào)登錄 頁(yè)面:
class MainActivity : ComponentActivity() {
private val isLogInning = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LoginScreen(isLogInning)
}
}
private fun login() {
isLogInning.value = true
Toast.makeText(this, "登錄中", Toast.LENGTH_SHORT).show()
}
@Composable
fun LoginScreen(isLogInning: MutableState<Boolean> = mutableStateOf(false)) {
Column {
Image(
painter = painterResource(R.drawable.titlebar_back_light),
contentDescription = null,
modifier = Modifier
.height(40.dp)
.width(15.dp)
.absoluteOffset(10.dp)
)
Text(
text = "登錄TP-LINK ID",
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp)
)
TextField(
value = TextFieldValue(),
onValueChange = {},
placeholder = { Text(text = "TP-LINK ID") },
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White,
placeholderColor = Color.LightGray
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
)
TextField(
value = TextFieldValue(),
onValueChange = {},
placeholder = { Text(text = "密碼") },
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White,
placeholderColor = Color.LightGray
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
)
Text(
text = "忘記密碼",
fontSize = 16.sp,
color = Color.Gray,
textAlign = TextAlign.End,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 20.dp)
)
Button(
content = {
if (isLogInning.value)
Text("登錄中...")
else
Text("登錄")
},
onClick = { login() },
colors = ButtonDefaults.buttonColors(
backgroundColor =
if (isLogInning.value)
Color(0xFFA6B7F7)
else
Color(0xFF3C65FC),
contentColor = Color.White
),
modifier = Modifier
.height(65.dp)
.fillMaxWidth()
.padding(10.dp)
)
Row {
Text(
text = "新用戶注冊(cè)",
fontSize = 16.sp,
color = Color(0xFF3C65FC),
modifier = Modifier.padding(10.dp)
)
Text(
text = "暫不登錄",
fontSize = 16.sp,
color = Color.Gray,
modifier = Modifier
.padding(10.dp)
.offset(205.dp)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
LoginScreen()
}
}
簡(jiǎn)單使用
Column
做一個(gè)垂直線性布局.dp
是 Int 的擴(kuò)展函數(shù)伐脖,方便在代碼中直接定義以 dp 為單位的數(shù)值運(yùn)用
Modifier
的offSet
、padding
乐设、fillMaxWidth
等調(diào)整組件細(xì)節(jié)定義一個(gè)
MutableState
類(lèi)型的isLogInning
作為參數(shù)傳入 Composable 函數(shù)LoginScreen
讼庇,讓 Compose 自動(dòng)訂閱,自動(dòng)根據(jù)這個(gè)值的變化而重組LoginScreen
方法近尚,更新UI顯示當(dāng)?shù)卿洶粹o點(diǎn)擊時(shí)回調(diào)
onClick
方法蠕啄,改變isLogInning
的值當(dāng)
isLogInning
的值改變時(shí),能看到“登錄”按鈕自動(dòng)改變了顏色,并變?yōu)椤暗卿浿?..”歼跟,而我們并沒(méi)有主動(dòng)去命令它setText
和setBackground
和媳。這和Data Binding
類(lèi)似,但更加簡(jiǎn)單和靈活