Android Compose UI 自学总结

什么是 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 …
uiUI相关最基础的功能,比如绘制、测量、布局、触摸反馈等等Layout …
animation动画层,比如渐变、平移等等animate*AsState …
foundation基于开发者的根基层,比如自带的基础控件、完整的UI体系Image、Column、Row …
materialMaterial 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://www.jianshu.com/p/aecc9b99253a
作者:一迅

最后

如果有想要进军Jetpack comepose的友友,不妨来参考这篇《Android Jetpack Compose开发应用指南》,让你从入门到精通,顺利找到自己中意的岗位,需要的可以扫下方二维码免费领取~

《Android Jetpack Compose开发应用指南》

第一章 初识Jetpack

1.1 JetPack 是什么
1.2 JetPack 和AndroidX
1.3 AndroidX的迁移

在这里插入图片描述

第二章 Compose的设计原理和基本概念

2.1 JetPack Compose环境搭建
2.2 JetPack Compose新特性和组件依赖
2.3 JetPack Compose编程思想总结

在这里插入图片描述

第三章 Compose入门

3.1 JetPack Compose入门的基础案例
3.2 JetPack Compose基础实战

在这里插入图片描述

第四章 Compose布局

4.1 Compose State
4.2 Compose样式(Theme)
4.3 Compose布局核心控件
4.4 自定义布局
4.5 Compose中的 ConstraintLayout

在这里插入图片描述

第五章 Compose动画

5.1 Compose SideEffect
5.2 Compose动画概述
5.3 Compose Crossfade
5.4 Compose animateContentSize
5.5 Animatable
5.6 Compose自定义动画

在这里插入图片描述

第六章 Compose图形

6.1Compose Canvas
6.2 Compose 绘制API的分析
6.3 Compos自定义绘制

在这里插入图片描述

第七章 Compose核心控件总结

7.1 Scaffold
7.2 LazyColumn

在这里插入图片描述

第八章 Compose项目实战

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值