初識(shí)Jetpack Compose

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ù)覽能力

image.png
image.png
image.png

頂層函數(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多的包大小。


image.png

總結(jié)

聲明式UI使我們的代碼更加簡潔,這也是擁抱大前端一次很好的嘗試。Compose 確實(shí)是一套比較難學(xué)的東西,因?yàn)樗吘固乱蔡罅耍且粋€(gè)完整的、全新的框架,確實(shí)讓很多人感覺學(xué)不動(dòng),那怎么辦呢?學(xué)唄

學(xué)習(xí)資料

1.Compose官網(wǎng)

2.View 嵌套太深會(huì)卡?來用 Jetpack Compose,隨便套——Intrinsic Measurement

3.深入詳解 Jetpack Compose | 優(yōu)化 UI 構(gòu)建

4.官方視頻-Jetpack Compose Beta 版現(xiàn)已發(fā)布!

5.Jetpack Compose 使用前后對(duì)比

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 何為Compose 2019 年中,Google 在 I/O 大會(huì)上公布的用于Android構(gòu)建原生界面的全新 U...
    塞上牧羊空許約閱讀 843評(píng)論 0 2
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,937評(píng)論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn),也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 8,210評(píng)論 0 4

友情鏈接更多精彩內(nèi)容