美文网首页jetpackjetpack compose
使用Jetpack Compose构建Android UI

使用Jetpack Compose构建Android UI

作者: zhangke3016 | 来源:发表于2019-11-13 16:31 被阅读0次
    Jetpack Compose

    Jetpack Compose 是一个独立的 UI 工具包,它结合了响应式编程模型和 Kotlin 编程语言的简洁性和易用性,旨在简化 UI 开发。
    它是完全声明性的,意味着可以通过调用一系列将数据转换为UI的函数来描述UI。当基础数据更改时,框架会自动调用这些函数,从而更新视图层次结构。
    现在的版本还是 0.1.0-dev02,处于非常早期的版本,官方也再三强调非常有可能产生变化且无法用于生产环境。不过简单了解下 Compose 还是不错的。

    1. 准备

    要启动新的Compose项目,请打开Android Studio 4.0,然后选择启动新的Android Studio项目:


    Android Studio版本

    创建新项目时,从可用模板中选择“
    Empty Compose Activity”,注意
    minimumSdkVersion 至少为21及以上,“Language” 必须为kotlin:

    创建项目

    2. Jetpack Compose构建UI的特点

    API

    Button 继承自 TextView,理论上我们只需要一个文本 + 可点击的区域就可以了,但是由于 TextView 的特性,它本身是可以长按出现复制、选择功能的,但是一个 Button 要这些功能有什么用呢?Jetpack Compose 的核心: 组合优于继承,所有的 UI 都是通过组合实现,不存在继承关系。

    Code

    目前的 UI 构建方式来说,写一个自定义 View 需要实现测量和布局,响应用户的行为需要实现大量的 Listener 事件,同时还要配合 XML 自定义属性,非常繁琐。而且以目前的View代码量体积来说,想要完全优化重构是不现实的。发布一个全新的 UI 构建库,从根本上解决问题,所以 Google 推出了全新的 Android UI 组件库 Jetpack Compose。

    Jetpack Compose 试图改变原有的 UI 构建方式,同时带来以下 4 点全新的改变:


    Goals
    1. UI 的变化更新不再跟随 Android 大版本的发布而更新
    2. 编写 UI 代码不需要掌握庞大繁琐的技术栈
    3. 简单直接的状态控制以及用户行为处理
    4. 使用更少的代码来编写 UI

    说了这么多,用一下看看吧。

    3. 使用Compose构建UI

    新创建好的MainActivity长这样:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MaterialTheme {
                    Greeting("Android")
                }
            }
        }
    }
    
    @Composable
    fun Greeting(name: String) {
        Text(text = "Hello $name!")
    }
    
    @Preview
    @Composable
    fun DefaultPreview() {
        MaterialTheme {
            Greeting2("Android")
        }
    }
    

    使用setContent用来定义布局,但不是使用XML文件,而是在其中调用Composable函数。要创建可组合函数,只需将@Composable注释添加到函数。该函数可以调用其他的@Composable函数。

    @Composable
    fun Greeting(name: String) {
       Text(text = "Hello $name!") //Text是library提供的可组合函数。
    }
    

    可组合函数是带有@Composable注释标记的Kotlin函数

    @Preview("Text preview")
    @Composable
    fun DefaultPreview() {
        Greeting(name = "Android")
    }
    

    @Preview标记任何一个无参数的Composable函数并Build项目,就可以在Android Studio中看到预览。

    Text preview
    刷新UI

    遵循单一职责原则。@Composable函数负责单个功能,该功能完全由该函数封装。例如,如果要为某些组件设置背景色,则必须使用Surface可组合功能。

    Text设置背景色,我们需要定义一个Surface包裹它。

    @Composable
    fun Greeting(name: String) {
        Surface(color = Color.Yellow) {
            Text (text = "Hello $name!")
        }
    }
    
    Text preview

    Modifiers
    Modifiers是为UI组件提供其他修饰的属性列表。目前可用的修饰符有:SpacingAspectRatio和修改Flexible Layouts布局的RowColumn

    @Composable
    fun Greeting(name: String) {
        Surface(color = Color.Yellow) {
            //Spacing 为文本添加填充
            Text(text = "Hello $name!", modifier = Spacing(24.dp)) 
        }
    }
    

    点击Build & Refresh按钮查看预览:

    Modifiers

    请注意,@Composable注释仅对创建UI的函数是必需的。它可以调用常规函数和其他Composables函数。如果某个功能不满足这些要求,则不应使用@Composable注解。

    创建通用Container

    @Composable
    fun MyApp(child: @Composable() () -> Unit) {
        MaterialTheme {
            Surface(color = Color.Yellow) {
                child()
            }
        }
    }
    

    该函数以Composable函数(在此称为)的 lambda 作为参数,该 lambda child返回Unit。我们返回Unit是因为所有Composable函数都必须返回Unit

    @Composable()将 Composable 函数用作参数时,需要添加注解:
    fun MyApp(child: @Composable() () -> Unit) { ... }

    后面代码就可以如此调用:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp {
                    Greeting("Android")
                }
            }
        }
    }
    
    @Preview("Text preview")
    @Composable
    fun DefaultPreview() {
        MyApp {
            Greeting("Android")
        }
    }
    

    将UI组件提取到Composable函数中,以便我们可以重复使用它们而无需复制代码。比如使用不同的参数重用同一Composable函数。以垂直顺序排列,我们使用ColumnComposable函数(类似于垂直LinearLayout)。

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp {
                    MyScreenContent()
                }
            }
        }
    }
    
    @Composable
    fun MyScreenContent() {
        Column {
            Greeting("Android")
            Divider(color = Color.Black)
            Greeting("there")
        }
    }
    
    @Composable
    fun Greeting(name: String) {
        Text (text = "Hello $name!", modifier = Spacing(24.dp))
    }
    
    @Preview("MyScreen preview")
    @Composable
    fun DefaultPreview() {
        MyApp {
            MyScreenContent()
        }
    }
    

    Divider 是提供的可组合函数,用于创建水平分隔线。

    preview

    可以像Kotlin中的任何其他函数一样调用compose函数。可以添加语句来影响UI的显示方式,构建UI非常方便。

    @Composable
    fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
        Column {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
    }
    
    preview

    不知道你有没有这种想法,这里的for循环会不会就是把 Text 翻译为 TextView,然后此方法就是接收一个 List<String> 对象,返回一个 List<TextView>?
    显示布局边界看下:


    preview

    事实并非如此,它所有的可绘制元素都不是 Android 原生的 View,其顶层View为AndroidComposeView,内部在维护的 ComponentNode负责绘制。


    AndroidComposeView

    数据流
    通过将对象作为参数传递给Composable函数,数据向下流动。

    @Composable 
    fun MyExampleFunction(items: List<Item>) {
        Column {
            for (item in items) {
                RenderItem(item = item)
            } 
        }
    }
    
    @Composable
    fun RenderItem(item: Item) {
        Row {
            Text(text = item.name)
            WidthSpacer(4.dp)
            Text(text = item.description)
        }
    }
    

    RenderItem从调用Composable函数接收其所需的数据作为参数。如果我们要处理Item单击,则使用lambda 将信息从层次结构的底部传递到顶部。

    @Composable 
    fun MyExampleFunction(items: List<Item>, onSelected: (Item) -> Unit) {
        Column {
            for (item in items) {
                RenderItem(item = item, onClick = { onSelected(item) })
            } 
        }
    }
    
    @Composable
    fun RenderItem(item: Item, onClick: () -> Unit) {
        Clickable(onClick = onClick) {
            Row {
                Text(text = item.name)
                WidthSpacer(4.dp)
                Text(text = item.description)
            }
        }
    }
    
    数据随参数向下流动,事件随lambda向上流动

    数据随参数向下流动,事件随lambda向上流动。

    使用@Model管理状态

    对状态更改做出反应是Compose的核心。如果数据发生更改,则可以使用新数据调用Composable函数将数据转换为UI,从而更新UI。
    Compose使用自定义的Kotlin编译器插件,当基础数据发生更改时,可以重新调用函数以更新UI视图。
    Compose提供了@Model注解,该注解可以放在任何类上。如果数据发生更改,从@Model参数读取值的可组合函数将自动被调用。该@Model注解将导致编译器重写类,使它可观察和线程安全。可组合函数将自动订阅它读取的类的任何可变变量。如果它们发生变化,将重新组合读取这些字段。
    举个栗子,比如做一个计数器,跟踪用户单击多少次Button:

    @Model 
    class CounterState(var count: Int = 0)
    

    在CounterState加上注解@Model,任何将此类作为参数的Composable函数在count值更改时将自动重新组合。定义Counter为一个Composable函数,该函数采用CounterState一个参数,并发出Button,显示单击了多少次。

    @Composable 
    fun Counter(state: CounterState) {
        Button(
            text = "I've been clicked ${state.count} times",
            onClick = {
                state.count++
            }
        )
    }
    

    每次count更改时,Button都会重新构成并显示的新值count。

    @Composable
    fun MyScreenContent(
        names: List<String> = listOf("Android", "there"),
        counterState: CounterState = CounterState()
    ) {
        Column {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
            Divider(color = Color.Transparent, height = 32.dp)
            Counter(counterState)
        }
    }
    
    Model

    感觉有种JS上Object.setProperty的即时感,确实如官方所说, Jetpack Compose 受到了 React、Litho、Vue、Flutter 的启发。

    布局

    列和行的主轴和横轴
    与屏幕中心对齐,我们可以使用列的crossAxisAlignment参数:
    @Composable
    fun MyScreenContent(
        names: List<String> = listOf("Android", "there"),
        counterState: CounterState = CounterState()
    ) {
        Column(crossAxisAlignment = CrossAxisAlignment.Center) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
            Divider(color = Color.Transparent, height = 32.dp)
            Counter(counterState)
        }
    }
    
    @Preview("MyScreen preview")
    @Composable
    fun DefaultPreview() {
        MyApp {
            MyScreenContent()
        }
    }
    

    刷新预览:


    preview
    @Composable
    fun Counter(state: CounterState) {
        Button(
            text = "I've been clicked ${state.count} times",
            onClick = {
                state.count++
            },
            style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
        )
    }
    
    preview

    Compose 对 ConstraintLayout 的支持还正在进行中。对于现有的布局控件,以后应该都是会添加支持的。

    Compose 提供了 VerticalScroller 和 HorizontalScroller 来生成列表,在使用上与 RecyclerView 是完全不同的体验:

    @Composable
    fun MyApp() {
        VerticalScroller {
            Column {
                repeat(20) {
                    Row(mainAxisSize = LayoutSize.Expand) {
                        Container(height = 48.dp) {
                            Text("Item $it", modifier = Spacing(left = 16.dp))
                        }
                    }
                }
            }
        }
    }
    

    事实上,Scroller 与 ScrollView 更接近,只提供了一个滚动的功能,并没提到有对 View 进行回收复用。

    兼容现有UI的构建方式
    上图:

    GenerateView

    使用 Jetpack Compose 编写的 View,可以无缝的通过 xml 在原有视图上使用,只需要增加一个 @GenerateView 注解。

    GenerateView

    原有的 View 也支持 Jetpack Compose 写法。目前在预览版里@GenerateView注解还无法使用,不免有些遗憾~

    自定义view

    自定义view
    @Preview
    @Composable
    fun errorView() {
        val checkBox = @Composable {
            Draw { canvas: Canvas, parentSize: PxSize ->
                val size = parentSize.width.value
                val outer = RRect(0f,0f,size,size).withRadius(Radius(10f, 10f))
                canvas.drawRRect(outer, Paint().apply {
                    color = Color.Red
                })
            }
    
            Draw { canvas: Canvas, parentSize: PxSize ->
                val paint = Paint().apply {
                    color = Color.White
                    strokeCap = StrokeCap.round
                    strokeWidth = 10f
                    isAntiAlias = true
                }
                val size = parentSize.width.value
                val leftStart = Offset(size / 4, size / 4)
                val leftEnd = Offset(size / 4 * 3, size / 4 * 3)
                val rightStart = Offset(size / 4 * 3, size / 4)
                val rightEnd = Offset(size / 4, size / 4 * 3)
                canvas.drawLine(leftStart, leftEnd, paint = paint)
                canvas.drawLine(rightStart, rightEnd, paint = paint)
            }
        }
        Layout(children = checkBox) { _, _->
            layout(IntPx(200), IntPx(200)){}
        }
    }
    

    Jetpack Compose 中实现自定义 View 的过程也非常简单,我们只需要关注 Draw 和 Layout 这两个方法就好了,绘制过程和之前一样,还是经过 measure、layout、draw ,但写法很精简。

    Jetpack Compose带给我们一种Android新的构建UI方式的实践,从语法上来看还是有些Flutter的影子,声明式UI和数据驱动带给我们更多想象力。在未来的计划中,Jetpack Compose 会支持 Kotlin 协程、会支持现有的 Android Arch Componet 、会有更完善的动画机制。一起期待吧~

    相关文章

      网友评论

        本文标题:使用Jetpack Compose构建Android UI

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