Jetpack Compose 是什么
Jetpack Compose是Google推出的一個(gè)新的UI工具包,旨在幫助開發(fā)者更快、更輕松地在Android 平臺(tái)上構(gòu)建Native應(yīng)用。Jetpack Compose是一個(gè)聲明式的UI框架,它提供了現(xiàn)代化的聲明式Kotlin API(取代Android 傳統(tǒng)的xml布局),可幫助開發(fā)者用更少的代碼構(gòu)建美觀、響應(yīng)迅速的應(yīng)用程序。
2019 年,Google 在 I/O 大會(huì)上公布了 Android 最新的 UI 框架:Jetpack Compose。Compose 可以說是 Android 官方有史以來動(dòng)作最大的一個(gè)庫了。它在 2019 年中就公布了,但要到今年也就是 2021 年才會(huì)正式發(fā)布。這兩年的時(shí)間 Android 團(tuán)隊(duì)在干嘛?在開發(fā) Compose。一個(gè) UI 框架而已,為什么要花兩年來打造呢?因?yàn)?Compose 并不是像 RecyclerView、ConstraintLayout 這種做了一個(gè)或者幾個(gè)高級(jí)的 UI 控件,而是直接拋棄了我們寫了 N 年的 View 和 ViewGroup 那一套東西,從上到下擼了一整套全新的 UI 框架。直白點(diǎn)說就是,它的渲染機(jī)制、布局機(jī)制、觸摸算法以及 UI 的具體寫法,全都是新的。
基于View UI體系有哪些痛點(diǎn)
歷史包袱,10多個(gè)大版本的迭代,View類已經(jīng)3w多行,而絕大部分的UI控件都繼承于View。意味你寫一個(gè)按鈕或者一個(gè)TextView都會(huì)受這個(gè)父類影響,繼承了很多沒有用到的特性和功能;
解析xml的額外開銷,而且需要反射創(chuàng)建對(duì)象 ;
預(yù)覽和Reload不方便,和Flutter毫秒級(jí)的hot reload完全不能比;
布局嵌套層級(jí)過深導(dǎo)致的性能問題,比如LinearLayout 二次測(cè)量或者三次測(cè)量問題。
Compose特點(diǎn)
聲明式
上面有一個(gè)詞:聲明式 ,那么什么是聲明式?假設(shè)我們需要在界面 上顯示一個(gè)文本
命令式方式:
1、首先需要一個(gè)xml文件,里面有一個(gè)TextView
...
<TextView
android:id="@+id/my_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
2、通過findViewById獲取到TextView控件
TextView textView = findViewById<TextView>(R.id.my_text);
3、通過setText()更新數(shù)據(jù),顯示到界面
textView.setText(content);
聲明式方式:
@Composable
fun Greeting() {
val count = remember { mutableStateOf(0) }
Column{
Button(onClick = { count.value++ }) {
Text("I've been clicked ${count.value} times")
}
}
}
為什么第一種方式是命令式,第二種方式是聲明式?主要體現(xiàn)在界面更新上,命令式下:數(shù)據(jù)更新時(shí),Java代碼手動(dòng)調(diào)用xml組件引用來更新界面,也就是Java代碼命令xml界面更新,這就是命令方式。而聲明式呢?只描述界面,當(dāng)數(shù)據(jù)狀態(tài)更新時(shí),自動(dòng)更新界面,這就是聲明式。
簡短總結(jié):
命令式是操作界面 (How);
聲明式是描述界面 (What)。
除了Jetpack Compose ,F(xiàn)lutter,React-Native,Swift-UI 都是聲明式的,這也是現(xiàn)在的一種趨勢(shì)。
強(qiáng)大的UI預(yù)覽能力



頂層函數(shù)
Compose是一個(gè)聲明式UI系統(tǒng),其中,我們用一組函數(shù)來聲明UI,并且一個(gè)Compose函數(shù)可以嵌套另一個(gè)Compose函數(shù),并以樹的結(jié)構(gòu)來構(gòu)造所需要的UI。在此過程中,Compose函數(shù)始終根據(jù)接收到的輸入生成相同的UI,因此,放棄類結(jié)構(gòu)不會(huì)有任何害處。從類結(jié)構(gòu)構(gòu)建UI過渡到頂層函數(shù)構(gòu)建UI對(duì)開發(fā)者和Android 團(tuán)隊(duì)都是一個(gè)巨大的轉(zhuǎn)變。
@Composable
fun checkbox ( ... ) //錯(cuò)誤的命名,應(yīng)該大寫開頭
@Composable
fun TextView ( ... )
@Composable
fun Edittext ( ... )
@Composable
fun Image ( ... )
Jetpack Compose首選組合而不是繼承,Android中的幾乎所有組件都繼承于View類(直接或間接繼承)。比如EidtText 繼承于TextView,而同時(shí)TextView又繼承于其他一些View,這樣的繼承結(jié)構(gòu)最終會(huì)指向跟View
而Compose團(tuán)隊(duì)則將整個(gè)系統(tǒng)從繼承轉(zhuǎn)移到了頂層函數(shù)。 Textview , EditText , 復(fù)選框 和所有UI組件都是 它們自己的Compose函數(shù),而它們構(gòu)成了要?jiǎng)?chuàng)建UI的其他函數(shù),代替了從另一個(gè)類繼承。
重組
在命令式界面模型中,如需更改某個(gè)微件,您可以在該微件上調(diào)用 setter 以更改其內(nèi)部狀態(tài)。在 Compose 中,您可以使用新數(shù)據(jù)再次調(diào)用可組合函數(shù)。這樣做會(huì)導(dǎo)致函數(shù)進(jìn)行重組 -- 系統(tǒng)會(huì)根據(jù)需要使用新數(shù)據(jù)重新繪制函數(shù)發(fā)出的微件。Compose 框架可以智能地僅重組已更改的組件。重組整個(gè)界面樹在計(jì)算上成本高昂,Compose 使用智能重組來解決此問題。
重組是指在輸入更改時(shí)再次調(diào)用可組合函數(shù)的過程,Compose 可以高效地重組。
可組合函數(shù)可能會(huì)像每一幀一樣頻繁地重新執(zhí)行,例如在呈現(xiàn)動(dòng)畫時(shí)??山M合函數(shù)應(yīng)快速執(zhí)行,以避免在播放動(dòng)畫期間出現(xiàn)卡頓。如果您需要執(zhí)行成本高昂的操作(例如從共享偏好設(shè)置讀取數(shù)據(jù)),請(qǐng)?jiān)诤笈_(tái)協(xié)程中執(zhí)行,并將值結(jié)果作為參數(shù)傳遞給可組合函數(shù)。
當(dāng)您在 Compose 中編程時(shí),有許多事項(xiàng)需要注意:
- 可組合函數(shù)可以按任何順序執(zhí)行;
- 可組合函數(shù)可以并行執(zhí)行;
- 重組會(huì)跳過盡可能多的可組合函數(shù)和 lambda;
- 重組是樂觀的操作,可能會(huì)被取消;
- 可組合函數(shù)可能會(huì)像動(dòng)畫的每一幀一樣非常頻繁地運(yùn)行。
示例
和flutter比較像,https://flutter.cn/docs/development/ui/widgets-intro
/**
* Colume , Row ,Box
*/
@Preview(showBackground = true)
@Composable
fun DemoLayout() {
Row(
modifier = Modifier
.size(200.dp)
.background(Color.Yellow),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Box(
Modifier
.size(50.dp)
.background(Color.Red)
)
Box(
Modifier
.size(50.dp)
.background(Color.Blue)
)
Column(
Modifier
.size(100.dp)
.background(Color.Cyan)
) {
Text("Android")
Text(
"iOS"
)
Text(
"H5",
Modifier
.background(Color.Green),
fontSize = 15.sp
)
}
}
}
/**
* Text
*/
@Preview(showBackground = true)
@Composable
fun DemoText() {
val txt = remember { mutableStateOf(0) }
Text(
text = "${txt.value}",
Modifier
.background(Color.Magenta)
.size(200.dp, 200.dp)
.clickable(
enabled = true,
role = Role.Button
) {
txt.value += 1
},
fontStyle = FontStyle.Italic,
fontWeight = FontWeight(1000),
fontFamily = FontFamily.SansSerif,
letterSpacing = 10.sp,
textDecoration = TextDecoration.Underline,
textAlign = TextAlign.Center,
lineHeight = 20.sp,
maxLines = 3,
softWrap = true,
overflow = TextOverflow.Clip,
)
}
/**
* AppendText
*/
@Preview(showBackground = true)
@Composable
fun DemoAppendText() {
Text(
buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color.Blue,
fontWeight = FontWeight.Bold
)
) {
append("Jetpack ")
}
append("Compose ")
withStyle(
style = SpanStyle(
color = Color.Red,
fontWeight = FontWeight.Bold,
fontSize = 30.sp
)
) {
append("is ")
}
append("wonderful")
}
)
}
/**
* List
*/
@ExperimentalFoundationApi
@Preview(showBackground = true)
@Composable
fun DemoLazyColumn() {
Box {
val listState = rememberLazyListState()
LazyColumn(
Modifier.size(200.dp),
state = listState
) {
stickyHeader {
Text(text = "stickyHeader")
}
// Add a single item
item {
Text(text = "First item")
}
// Add 50 items
items(50) { index ->
Text(text = "Item: $index")
}
// Add another single item
item {
Text(text = "Last item")
}
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 10
}
}
Text(
text = if (showButton) {
"".plus(showButton)
} else {
"".plus(showButton)
},
modifier = Modifier
.size(30.dp)
.background(Color.Yellow)
)
}
}
/**
* Image, 圖片庫用coil : https://zhuanlan.zhihu.com/p/287752448
*/
@Preview(showBackground = true)
@Composable
fun ImageDemo() {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
)
}
/**
* Canvas
*/
@Preview(showBackground = true)
@Composable
fun CanvasDemo() {
Canvas(
modifier = Modifier
.height(300.dp)
.width(300.dp)
) {
val canvasWidth = size.width
val canvasHeight = size.height
drawCircle(
color = Color.Blue,
center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
radius = size.minDimension / 4
)
drawLine(
start = Offset(x = canvasWidth, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Blue,
strokeWidth = 5F
)
drawLine(
start = Offset(x = 0f, y = 0f),
end = Offset(x = canvasWidth, y = canvasHeight),
color = Color.Blue,
strokeWidth = 5F
)
rotate(degrees = 45F) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = size / 3F
)
}
}
}
/**
* 手勢(shì)
*/
@Preview(showBackground = true)
@Composable
fun GestureDemo() {
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
}
enum class BoxState { Collapsed, Expanded }
/**
* 動(dòng)畫, https://developer.android.com/codelabs/jetpack-compose-animation#3
*/
@Preview(showBackground = true)
@Composable
fun AnimatingBox(boxState: BoxState = BoxState.Expanded) {
val transitionData = updateTransitionData(boxState)
// UI tree
Box(
modifier = Modifier
.background(transitionData.color)
.size(transitionData.size)
)
}
// Holds the animation values.
private class TransitionData(
color: State<Color>,
size: State<Dp>
) {
val color by color
val size by size
}
// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 32.dp
BoxState.Expanded -> 300.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
遇到問題
1.如果要追蹤具體的實(shí)現(xiàn),需要反編譯代碼;
2.Preview功能還需要進(jìn)一步增強(qiáng),由于要實(shí)現(xiàn)實(shí)時(shí)預(yù)覽,每次修改Compose都需要編譯,如果項(xiàng)目比較大,編譯時(shí)間很長,那體驗(yàn)就會(huì)很差了;
3.某些API設(shè)計(jì)上有些混淆,比如Text AlignText只能設(shè)置水平居中;
4.引入Compose會(huì)帶來3M多的包大小。

總結(jié)
聲明式UI使我們的代碼更加簡潔,這也是擁抱大前端一次很好的嘗試。Compose 確實(shí)是一套比較難學(xué)的東西,因?yàn)樗吘固乱蔡罅耍且粋€(gè)完整的、全新的框架,確實(shí)讓很多人感覺學(xué)不動(dòng),那怎么辦呢?學(xué)唄
學(xué)習(xí)資料
2.View 嵌套太深會(huì)卡?來用 Jetpack Compose,隨便套——Intrinsic Measurement
3.深入詳解 Jetpack Compose | 優(yōu)化 UI 構(gòu)建