Android Compose UI 自学总结

作者: 一迅 | 来源:发表于2021-06-21 11:05 被阅读0次

    什么是 Jetpack Compose

    Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。

    2018年初就开始设计工作,2019年公开。

    属于全新的UI库,Jetpack系列中的一员。

    重新定义了Android编写Ui的方式,采用声明式开发。

    还设计了Compose使用入门的文字视频教学,Google GDG还在B站发布了Compose系统式教学

    写法对比

    原写法

    <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="Hello"/>
    
    TextView textview = new TextView(this);
    textview.setText("Hello");
    
    val textview = TextView(this)
    textview.text = "Hello"
    

    声明式写法

    Text(text = "Hello")
    

    区别

    1. 原写法更新数据需要手动更新,而声明式UI自动更新
    2. 声明式UI不需要xml

    配置

    新项目

    安装Android Studio Preview版本,新建项目选择Empty Compose Activity

    Android Studio Preview.png

    老项目

    1. 引入相关Compose UI依赖包 和 添加Compose配置
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    ......
    
    1. 直接新建一个新项目,再把新项目默认的配置都拷贝到老项目。
    2. https://developer.android.com/jetpack/compose/interop

    各组件对比

    TextView

    Text(text = "Hello Compose")
    

    ImageView

    Image(
        painterResource(R.drawable.ic_launcher_background),
        contentDescription = "Image"
    )
    
    // Bitmap
    // 并非Android原生Bitmap,是Compose独立于平台的Bitmap
    // Canvas也是如此
    Image(ImageBitmap = , contentDescription = "")
    // 矢量图
    Image(imageVector = , contentDescription = "")
    

    google 整理了用于compose 加载网络图片库

    // Coil 官方目前推荐的
    // 支持kotlin特性(扩展函数、协程)
    // implementation "com.google.accompanist:accompanist-coil:<version>"
    CoilImage("https://***.jpg", contentDescription = "")
    
    // Glide
    // 用的人多
    
    // Picasso
    // 官方已经移除了,描述是Picasso导致代码CI检测失效了,而且用的人少,不打算维护了
    

    Layout

    // FrameLayou
    // 一层一层叠加
    Box() {
        Text(text = "Text1")
        Text(text = "Text2")
        Text(text = "Text3")
    }
    
    // LinearLayout
    // 纵向排列
    Column() { 
        Text(text = "")
        Image(bitmap =, contentDescription =)
        CoilImage(data =, contentDescription =)
    }
    
    // 横向排列
    Row() {
        Text(text = "")
        Image(bitmap =, contentDescription =)
        CoilImage(data =, contentDescription =)
    }
    
    布局预览图.png

    RecyclerView

    // 纵向
    LazyColumn {
        items(listOf(1, 2, 3, 4, 5, 6)) { item ->
            Text(text = "item $item")
        }
    }
    
    // 横向
    LazyRow {
        items(listOf(1, 2, 3, 4, 5, 6)) { item ->
            Text(text = "item $item")
        }
    }
    
    recyclerview 预览图.png

    更多各组件对比 可以参考该网站
    https://www.jetpackcompose.app/What-is-the-equivalent-of-android:background-in-Jetpack-Compose

    Modifier

    Compose很重要的属性,用来控制UI的边距、背景、颜色、宽高、点击监听等等

    Padding

    Row(Modifier.padding(16.dp)) {
        Text(text = "Text4")
        Text(text = "Text5")
        Text(text = "Text6")
    }
    
    Row(Modifier
        .background(Color.Red)
        .padding(16.dp)) {
        Text(text = "Text4")
        Text(text = "Text5")
        Text(text = "Text6")
    }
    
    Row(Modifier
        .padding(16.dp)
        .background(Color.Red)) {
        Text(text = "Text4")
        Text(text = "Text5")
        Text(text = "Text6")
    }
    
    Row(Modifier
        .padding(16.dp)
        .background(Color.Red)
        .padding(16.dp)) {
        Text(text = "Text4")
        Text(text = "Text5")
        Text(text = "Text6")
    }
    

    Compose没有设置外边距的地方是因为不需要,用Padding就能实现。

    跟原生UI不一样,重复调用setPadding、setBackground,原生会进行覆盖。

    而Compose UI则是下发式一层一层传递处理,不会丢失上一次处理结果,变得很灵活。

    所以如果要设置外边距,先padding,再处理其他;

    设置一个背景多个不同点击事件,隔层次设置clickable即可。

    padding 预览图.png

    background

    // 背景圆角
    Row(
        Modifier
            .padding(16.dp)
            .background(Color.Red, RoundedCornerShape(16.dp))
            .padding(16.dp)
    ) {
        Text(text = "Text4")
        Text(text = "Text5")
        Text(text = "Text6")
    }
    
    // 背景切圆
    Row(
        Modifier
            .padding(16.dp)
            .background(Color.Red, RoundedCornerShape(16.dp))
            .padding(16.dp)
        ) {
        Text(text = "Text4")
        Text(text = "Text5")
        Text(text = "Text6")
    
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "Clip Test",
            Modifier.clip(CircleShape)
        )
    }
    
    background 预览图.png

    androidx.compose.foundation.shape

    自带了部分Shape

    自带 shape

    layout_width / layout_height

    // 分开设置宽高
    Modifier.width(100.dp).height(100.dp)
    
    // 同步设置宽高
    Modifier.size(100.dp)
    
    // 传统xml必须填写layout_width & layout_height
    // Compose中可以不写,默认宽高都是wrap_content
    
    // 如果需要match_parent,则需要手动设置
    Modifier.fillMaxWidth()
    Modifier.fillMaxHeight()
    
    // 宽高撑满
    Modifier.fillMaxSize()
    

    TextSize / TextColor

    // 设置文字大小和颜色,跟常规通用属性不太一样。
    // 在Modifier里面根本找不到设置的方法,查看Text()的参数发现是属于函数参数
    @Composable
    fun Text(
        text: String,
        modifier: Modifier = Modifier,
        color: Color = Color.Unspecified,
        fontSize: TextUnit = TextUnit.Unspecified,
        fontStyle: FontStyle? = null,
        fontWeight: FontWeight? = null,
        fontFamily: FontFamily? = null,
        letterSpacing: TextUnit = TextUnit.Unspecified,
        textDecoration: TextDecoration? = null,
        textAlign: TextAlign? = null,
        lineHeight: TextUnit = TextUnit.Unspecified,
        overflow: TextOverflow = TextOverflow.Clip,
        softWrap: Boolean = true,
        maxLines: Int = Int.MAX_VALUE,
        onTextLayout: (TextLayoutResult) -> Unit = {},
        style: TextStyle = LocalTextStyle.current
    ) {
      
        ...
      
    }
    

    setOnClickListener

    Row(
        Modifier
            .clickable { Unit }
            .padding(16.dp)
            .background(Color.Red, RoundedCornerShape(16.dp))
            .clickable { Unit }
            .padding(16.dp)
    ) {
        Text(text = "Click")
    
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "Click Test",
            Modifier.clip(CircleShape).clickable { Unit }
        )
    }
    
    clickable 预览图.png

    设置点击事件有一种不需要用Modifier.clickable

    Button属于为了点击事件而生的控件,默认提供了onClick

    Button(
        onClick = {
            // Logic
        }
    ) {
        Text(text = "默认onClick")
    }
    

    并且Button并不是给你提供一个Button样式,默认就是一个空壳,所以找不到设置按钮文本的地方。

    查看Button源码得知,需要自己去添加Button中的content,它只是给你一个默认提供onClick的布局。帮我们设置了Modifier.clickable,并且是一个Row布局。

    相当于原生Button,如果要设置DrawableLeft/DrawableRight,Compose的Button更灵活。

    Button 源码

    如何判断需要设置的属性在Modifier还是函数参数?

    通用设置先在Modifier里面找

    单一性设置在函数参数里面找(比如 Text)

    分层设计

    由下至上 说明 运用
    compiler 基于Kotlin的编译器插件 处理Composable函数
    runtime 最底层的概念模型,比如数据结构、状态管理等等 mutableStateOf、remember ...
    ui UI相关最基础的功能,比如绘制、测量、布局、触摸反馈等等 Layout ...
    animation 动画层,比如渐变、平移等等 animate*AsState ...
    foundation 基于开发者的根基层,比如自带的基础控件、完整的UI体系 Image、Column、Row ...
    material Material Design 风格层 Button ...

    实际开发过程中引用包,引用了一个material包就可以了

    如果不需要Material Design风格,就引用foundation包

    需要单独引用的包有预览功能包(ui-tool)、Material Design风格Icon扩展包(material-icons-extended)

    状态订阅与自动更新

    MutableState

    先用一个例子来看看传统写法和声明式写法的自动更新

    @Composable
    fun MyButton(btnText: String, callback: () -> Unit) {
        Button(onClick = callback) {
            Text(text = btnText)
        }
    }
    

    先写一个共用的MyButton函数空间

    参数为按钮文字和点击监听

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        // 常规状态变量
        var count1 = 1
        // Compose状态变量
        var count2 = mutableStateOf(1)
    
        setContent {
            Column(Modifier.verticalScroll(rememberScrollState())) {
                Text(text = "常规写法")
                Row {
                    Text(text = "count1 = $count1")
                    MyButton("累加count1") {
                        count1++
                    }
                }
              
                Divider()
    
                Text(text = "Compose mutableStateOf 写法")
                Row {
                    Text(text = "count2 = ${count2.value}")
                    MyButton("累加count1") {
                        count2.value ++
                    }
                }
              
                Divider()
            }
        }
    }
    
    

    根据现象得出结论

    常规状态变量被修改后,无法做到自动更新,而Compose状态变量会自动更新。

    并且自动更新后,会进行一个ReCompose,会让常规写法的状态变量被冻更新。

    源码解析

    通过源码得知,mutableStateOf(Value) 最终的实现是SnapshotMutableStateImpl,所以我们传递value后,他给我们的value的get/set 方法都加入了一个“钩子”。

    在每次修改值之后,就会触发set的“钩子”,新值同步到Snapshot中后,会同步其他调用过get的“钩子”的值

    image.png

    所以可以理解成,每次调用读就形成了一种快照,每次调用写后,Compose就会对所有记录过的快照进行一次通知,告诉他们我这个值改变了,然后这些快照就会把新的值更新到数据结构中,达成了界面自动更新。

    所以实际上我们操作的值,是操作这个value,并不是mutableStateOf返回的MutableState。

    // 写法就会变成
    // set
    MutableState.value = ***
    // get
    Text(text = MutableState.value)
    

    但是每次都要输入.value有点麻烦,官方提供了一个kotlin委托模式,把value的get/set委托给自己处理,不需要我们去管。

    var count3 by mutableStateOf(1)
    Text(text = "Compose mutableStateOf 委托模式写法")
    Row {
        Text(text = "count3 = $count3")
        MyButton("累加count3") {
            count3 ++
        }
    }
    

    Remember

    先看一段代码发现其中的问题

    Column(Modifier.verticalScroll(rememberScrollState())) {
        // 在Compose函数中创建MutableState
        var count4 by mutableStateOf(1)
    
        Text(text = "Compose mutableStateOf remember")
        Text(text = "count4 = $count4")
    
        MyButton("累加count4") {
            count4++
        }
    }
    

    根据现象发现

    MutableState的自动更新失效了,数据不变了。

    跟上个案例比较区别在于,创建MutableState一个在Compose函数之外,一个之内。

    为了验证到底ReCompose,在创建MutableState之前打印一句话。

    Column(Modifier.verticalScroll(rememberScrollState())) {
        println("刷新")
        // 在Compose函数中创建MutableState
        var count4 by mutableStateOf(1)
    
        Text(text = "Compose mutableStateOf remember")
        Text(text = "count4 = $count4")
    
        MyButton("累加count4") {
            count4++
        }
    }
    

    点击累加按钮发现,其实已经刷新了。

    问题出在Compose编译器插件再编译的过程中,对我们的代码做了修改,把可能会ReCompose的代码块包起来,提供一个返回值,再做一个标记把返回值存了起来,当触发ReCompose,Compose会从缓存区域根据标记找到返回值里面的代码块重新执行。

    而我们再累加count4的时候,触发了ReCompose。

    而取出来的代码块中 by mutableStateOf(1) 也是其中,所以被重新初始化了,导致上一次变量的值丢失了。

    再修改一下代码

    Column(Modifier.verticalScroll(rememberScrollState())) {
        println("刷新")
        // 在Compose函数中创建MutableState
        var count4 by mutableStateOf(1)
    
        Text(text = "Compose mutableStateOf remember")
        Button(onClick = { /*TODO*/ }) {
            Text(text = "count4 = $count4")
        }
    
        MyButton("累加count4") {
            count4++
        }
    }
    

    发现把Text套一层,就能正常自动更新值,并且没有重复打印 “刷新”

    原因在于Compose有一套界面刷新的算法机制,刷新的不是整个setContent{},而是单独的区域。

    但是在实际开发过程中,如果我们还要去分析去拆分去嵌套,会影响我们的开发,最关键的是,我们根本无法预测某个代码块会不会ReCompose。

    所以Compose提供了remember来解决这个问题,让编译器插件去处理这个问题。

    加上remember,把mutableStateOf(1)函数对象交给remember管理

    Column(Modifier.verticalScroll(rememberScrollState())) {
        println("刷新")
        // 在Compose函数中创建MutableState
        var count4 by remember { mutableStateOf(1) }
    
            Text(text = "Compose mutableStateOf remember")
        Text(text = "count4 = $count4")
    
        MyButton("累加count4") {
            count4++
        }
    }
    

    已经能正常显示了,并且也不需要手动干预去嵌套。

    remember会把我们的函数对象跟标记的代码包一起存储起来,根据自身界面刷新的算法来做预期之外的反复初始化。

    什么时候需要使用

    1. 可能需要ReCompose的情况下
    2. 还是全部都加上吧。。。(因为根本没办法判断你的代码块究竟会不会被ReCompose,哪怕你写的代码块清清楚楚,但是你也挡不住其他代码块会不会影响你被动ReCompose。所以关于什么时候需要使用remember这件问题,反而变得简单,遇到能包就包)

    参数

    var change = false
    var count5 by remember(change) { mutableStateOf(1) }
    
    
    // ... start logic ...
    change = true
    count5 ++
    // ... end logic ...
    

    remember是可以带参数的,如果下一次ReCompose或者执行带remember的Compose方法,参数如果没变,remember不会去重新计算。当参数变了,remember会重新初始化。

    remember 入口函数

    还可以绑定多个参数做逻辑处理

    List/Map 自动更新

    Text(text = "Compose mutableStateListOf remember")
    val count5 by remember {
        mutableStateOf(mutableListOf(1, 2, 3))
    }
    MyButton(btnText = "累加count5") {
        count5.add(count5.last() + 1)
        println("last value : ${count5.last()}")
    }
    for (i in count5) {
        Text(text = "count5 - item - $i")
    }
    

    mutableStateOf里面直接放一个MutableList,并且在累加的时候打印最后一个值

    发现居然count5 List里面的值变了,且用remember来防止重新被初始化,但是现象是没有自动更新。

    根据MutableState源码和打印的日志可以得住,要触发自动更新,setValue的“钩子”必须要执行,才能让Snapshot去通知刷新。而add(T)不会触发这个钩子,所以我们换一种写法再试试。

    Text(text = "Compose mutableStateListOf remember")
    var count5 by remember {
        mutableStateOf(mutableListOf(1, 2, 3))
    }
    MyButton(btnText = "累加count5") {
        // 不在原对象累加,创建一个新对象来添加新元素
        count5 = count5.toMutableList().apply {
            add(last() + 1)
        }
        println("last value : ${count5.last()}")
    }
    for (i in count5) {
        Text(text = "count5 - item - $i")
    }
    
    

    能解决我们的问题了,但是这样写总觉得代码看起来很奇怪,不太稳妥,每次都要改变对象触发“钩子”来ReCompose。

    所以你要用List来处理界面更新,就不要用mutableStateOf,改用mutableStateListOf,它内部帮我们处理关于List需要触发ReCompose的情况。

    写法上也就不能用by委托初始化了,因为不需要委托List的对象值变化了,只需要操作List内部对象值的变化,所以直接使用=

    Text(text = "Compose mutableStateListOf remember")
    val count5 = remember { mutableStateListOf(1, 2, 3) }
    MyButton(btnText = "累加count5") {
        count5.add(count5.last() + 1)
        println("last value : ${count5.last()}")
    }
    for (i in count5) {
        Text(text = "count5 - item - $i")
    }
    

    了解了List的写法和原理,再了解Map就很明白了

    Text(text = "Compose mutableStateMapOf remember")
    val count6 = remember { mutableStateMapOf(1 to "1", 2 to "2", 3 to "2") }
    MyButton(btnText = "累加count6") {
        count6[count6.size + 1] = "${count6.size + 1}"
    }
    for ((key, value) in count6) {
        Text(text = "count6 - item - $value")
    }
    

    State Hosting

    官方的字面意思是状态上提

    可以理解成作用域,在开发过程中遵守的规则

    看一段代码

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ..
            MyState()
            println(?)
        }
    }
    
    @Composable
    fun MyState() {
        Text(text = "MyState")
    }
    

    我如果需要拿到MyState中Text的值,其实是拿不到的。

    因为MyState是有内部状态,没有外部状态的函数控件,内部状态是"MyState"

    如果外部想拿到Text的值,就需要把MyState的内部状态上提。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val content by remember {
                mutableStateOf("MyState")
            }
            MyState(content)
            println(content)
        }
    }
    
    @Composable
    fun MyState(content: String) {
        Text(text = content)
    }
    

    状态上提后,就能拿到值,如果onCreate想拿这个值也拿不到了,因为setContent没有外部状态了。

    把val content再往上提一级,其实就可以了。

    理解其实很简单,主要是要遵守这一套写法。

    状态可以提到最上级,这样都能访问,但是这样会提高出错的概率,建议状态保持为满足需求开发中的最低一级,不要让不需要访问的一层能访问这个状态。

    学习资料

    // 官方教程
    https://developer.android.com/jetpack/compose
    
    // B站直播教程(有回放) B站UP主:上海GDG
    https://live.bilibili.com/21917305
    
    // 组建对照表
    https://www.jetpackcompose.app/What-is-the-equivalent-of-android:background-in-Jetpack-Compose
    
    // accompanist
    https://github.com/google/accompanist
    
    // github 开源项目
    https://github.com/MindorksOpenSource/Jetpack-Compose-Android-Examples
    

    相关文章

      网友评论

        本文标题:Android Compose UI 自学总结

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