Android Compose UI 自学总结

什么是 Jetpack Compose

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

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

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

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

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

写法对比

原写法


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:"
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 自学总结)