美文网首页
初识Jetpack Compose

初识Jetpack Compose

作者: 日月天逐 | 来源:发表于2021-07-12 19:35 被阅读0次

    Jetpack Compose 是什么

    Jetpack Compose是Google推出的一个新的UI工具包,旨在帮助开发者更快、更轻松地在Android 平台上构建Native应用。Jetpack Compose是一个声明式的UI框架,它提供了现代化的声明式Kotlin API(取代Android 传统的xml布局),可帮助开发者用更少的代码构建美观、响应迅速的应用程序。

    2019 年,Google 在 I/O 大会上公布了 Android 最新的 UI 框架:Jetpack Compose。Compose 可以说是 Android 官方有史以来动作最大的一个库了。它在 2019 年中就公布了,但要到今年也就是 2021 年才会正式发布。这两年的时间 Android 团队在干嘛?在开发 Compose。一个 UI 框架而已,为什么要花两年来打造呢?因为 Compose 并不是像 RecyclerView、ConstraintLayout 这种做了一个或者几个高级的 UI 控件,而是直接抛弃了我们写了 N 年的 View 和 ViewGroup 那一套东西,从上到下撸了一整套全新的 UI 框架。直白点说就是,它的渲染机制、布局机制、触摸算法以及 UI 的具体写法,全都是新的。

    基于View UI体系有哪些痛点

    • 历史包袱,10多个大版本的迭代,View类已经3w多行,而绝大部分的UI控件都继承于View。意味你写一个按钮或者一个TextView都会受这个父类影响,继承了很多没有用到的特性和功能;

    • 解析xml的额外开销,而且需要反射创建对象 ;

    • 预览和Reload不方便,和Flutter毫秒级的hot reload完全不能比;

    • 布局嵌套层级过深导致的性能问题,比如LinearLayout 二次测量或者三次测量问题。

    Compose特点

    声明式

    上面有一个词:声明式 ,那么什么是声明式?假设我们需要在界面 上显示一个文本

    命令式方式:
    1、首先需要一个xml文件,里面有一个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()更新数据,显示到界面

    textView.setText(content);
    

    声明式方式:

    @Composable
    fun Greeting() {
        val count = remember { mutableStateOf(0) }
        Column{
            Button(onClick = { count.value++ }) {
                Text("I've been clicked ${count.value} times")
            }
        }
    }
    

    为什么第一种方式是命令式,第二种方式是声明式?主要体现在界面更新上,命令式下:数据更新时,Java代码手动调用xml组件引用来更新界面,也就是Java代码命令xml界面更新,这就是命令方式。而声明式呢?只描述界面,当数据状态更新时,自动更新界面,这就是声明式。

    简短总结:

    • 命令式是操作界面 (How);

    • 声明式是描述界面 (What)。

    除了Jetpack Compose ,Flutter,React-Native,Swift-UI 都是声明式的,这也是现在的一种趋势。

    强大的UI预览能力

    image.png image.png image.png

    顶层函数

    Compose是一个声明式UI系统,其中,我们用一组函数来声明UI,并且一个Compose函数可以嵌套另一个Compose函数,并以树的结构来构造所需要的UI。在此过程中,Compose函数始终根据接收到的输入生成相同的UI,因此,放弃类结构不会有任何害处。从类结构构建UI过渡到顶层函数构建UI对开发者和Android 团队都是一个巨大的转变。

    @Composable
    fun checkbox ( ... ) //错误的命名,应该大写开头
      
    @Composable
    fun TextView ( ... )
      
    @Composable
    fun Edittext ( ... )
      
    @Composable
    fun Image ( ... )
    

    Jetpack Compose首选组合而不是继承,Android中的几乎所有组件都继承于View类(直接或间接继承)。比如EidtText 继承于TextView,而同时TextView又继承于其他一些View,这样的继承结构最终会指向跟View

    而Compose团队则将整个系统从继承转移到了顶层函数。 Textview , EditText , 复选框 和所有UI组件都是 它们自己的Compose函数,而它们构成了要创建UI的其他函数,代替了从另一个类继承。

    重组

    在命令式界面模型中,如需更改某个微件,您可以在该微件上调用 setter 以更改其内部状态。在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 -- 系统会根据需要使用新数据重新绘制函数发出的微件。Compose 框架可以智能地仅重组已更改的组件。重组整个界面树在计算上成本高昂,Compose 使用智能重组来解决此问题。

    重组是指在输入更改时再次调用可组合函数的过程,Compose 可以高效地重组。

    可组合函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。可组合函数应快速执行,以避免在播放动画期间出现卡顿。如果您需要执行成本高昂的操作(例如从共享偏好设置读取数据),请在后台协程中执行,并将值结果作为参数传递给可组合函数。

    当您在 Compose 中编程时,有许多事项需要注意:

    • 可组合函数可以按任何顺序执行;
    • 可组合函数可以并行执行;
    • 重组会跳过尽可能多的可组合函数和 lambda;
    • 重组是乐观的操作,可能会被取消;
    • 可组合函数可能会像动画的每一帧一样非常频繁地运行。

    示例

    和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
                )
            }
        }
    }
    
    /**
     * 手势
     */
    @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 }
    
    /**
     * 动画, 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.如果要追踪具体的实现,需要反编译代码;
    2.Preview功能还需要进一步增强,由于要实现实时预览,每次修改Compose都需要编译,如果项目比较大,编译时间很长,那体验就会很差了;
    3.某些API设计上有些混淆,比如Text AlignText只能设置水平居中;
    4.引入Compose会带来3M多的包大小。


    image.png

    总结

    声明式UI使我们的代码更加简洁,这也是拥抱大前端一次很好的尝试。Compose 确实是一套比较难学的东西,因为它毕竟太新也太大了,它是一个完整的、全新的框架,确实让很多人感觉学不动,那怎么办呢?学呗

    学习资料

    1.Compose官网

    2.View 嵌套太深会卡?来用 Jetpack Compose,随便套——Intrinsic Measurement

    3.深入详解 Jetpack Compose | 优化 UI 构建

    4.官方视频-Jetpack Compose Beta 版现已发布!

    5.Jetpack Compose 使用前后对比

    相关文章

      网友评论

          本文标题:初识Jetpack Compose

          本文链接:https://www.haomeiwen.com/subject/poddpltx.html