Jetpack Compose

笔记

入门教程: https://zhuanlan.zhihu.com/p/433581686
官网:Android Compose导读具体教程

  • Jetpack Compose:是个利用“声明式编程”构建Android原生界面(UI)的工具包
  • 环境要求:
    • ComposeApp仅支持 Kotlin
    • 最低 sdk 版本为21(Android5.0);
    • 可用Android Studio(2020-3-1以上)、Idea
    • jdk11以上
  • @Compose注解的方法,只能被@Compose注解的方法中调用
  • Modifier:设置UI位置padding
    • Modifier.plus(otherModifier) //把其他的modifier加入到当前的Modifier中
    • fillMaxHeight(填充整个高度)、fillMaxWidthfillMaxSize 类似于 match_patch、填充整个父布局

Jetpack-Compose

快速上手

基础知识官方demo

Compose是什么

Jetpack Compose:利用声明式编程构建Android原生界面(UI)的 工具包

Compose的优点

  • 更少的代码、代码量锐减
  • 强大的工具/组件支持
  • 直观的 Kotlin API
  • 简单易用

可组合函数

预览

布局

配置布局

Material Design

围绕ColorTypography(排版)、Shape(形状)这三大要素构建的。

列表和动画

布局

标准布局组件

@Compose

所有关于构建View的方法都必须添加@Compose注解才可以。并且@Compose协程的Suspend的使用方法比较类似,被@Compose注解的方法只能在同样被@Comopse解的方法中才能被调用。

1
2
3
4
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

预览@Preview

@Preview注解的方法可以在不运行App的情况下就可以确认布局的情况。

1
2
3
4
5
6
7
@Preview(showBackground = true,name = "Text UI",backgroundColor = 0xFF888888)
@Componse
private fun DefaultPreview(){
MyJetpackComposeTheme{
Greeting("Android")
}
}

常用的参数:

  • name: String: 为该Preview命名,该名字会在布局预览中显示。
  • showBackground: Boolean: 是否显示背景,true为显示。
  • backgroundColor: Long: 设置背景的颜色。
  • showDecoration: Boolean: 是否显示Statusbar和Toolbar,true为显示。
  • group: String: 为该Preview设置group名字,可以在UI中以group为单位显示。
  • fontScale: Float: 可以在预览中对字体放大,范围是从0.01。
  • widthDp: Int: 在Compose中渲染的最大宽度,单位为dp。
  • heightDp: Int: 在Compose中渲染的最大高度,单位为dp。
    上面的参数都是可选参数,还有像背景设置等的参数并不是对实际的App进行设置,只是对Preview中的背景进行设置,为了更容易看清布局。

    setContent

    setContent的作用是和Layout/View中的setContentView是一样的。
    setContent的方法也是有@Compose注解的方法。所以,在setContent中写入关于UI的@Compopse方法,即可在Activity中显示。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
    MyJetpackComposeTheme {
    // A surface container using the 'background' color from the theme
    Surface(color = MaterialTheme.colors.background) {
    Greeting("Android")
    }
    }
    }
    }

    主题Theme

    在创建新的Compose项目时会自动创建一个Theme.kt文件。 我们可以通过更改颜色来完成对主题颜色的设置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.zm.myjetpackcompose.ui.theme 

    import androidx.compose.foundation.isSystemInDarkTheme
    import androidx.compose.material.MaterialTheme
    import androidx.compose.material.darkColors
    import androidx.compose.material.lightColors
    import androidx.compose.runtime.Composable

    private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
    )

    private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200

    /* Other default colors to override
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black,
    */
    )

    @Composable
    fun MyJetpackComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
    val colors = if (darkTheme) {
    DarkColorPalette
    } else {
    LightColorPalette
    }

    MaterialTheme(
    colors = colors,
    typography = Typography,
    shapes = Shapes,
    content = content
    )
    }

    Modifier

    Modifier是各个Compose的UI组件一定会用到的一个类。它是被用于设置UI的摆放位置,padding等信息的类。
    padding
    设置各个UI的padding
    1
    2
    3
    4
    Modifier.padding(10.dp) // 给上下左右设置成同一个值 
    Modifier.padding(10.dp, 11.dp, 12.dp, 13.dp) // 分别为上下左右设值
    Modifier.padding(10.dp, 11.dp) // 分别为上下和左右设值
    Modifier.padding(InnerPadding(10.dp, 11.dp, 12.dp, 13.dp))// 分别为上下左右设值
    plus
    可以把其他的Modifier加入到当前的Modifier中。
    1
    Modifier.plus(otherModifier) // 把otherModifier的信息加入到现有的modifier中 
    fillMaxHeight、fillMaxWidth、fillMaxSize
    类似于match_parent、填充整个父layout。
    1
    Modifier.fillMaxHeight() // 填充整个高度 
    width、heigh、size
    设置Content的宽度和高度。
    1
    2
    3
    Modifier.width(2.dp) // 设置宽度 
    Modifier.height(3.dp) // 设置高度
    Modifier.size(4.dp, 5.dp) // 设置高度和宽度 复制代码
    widthIn、heightIn、sizeIn
    设置Content的宽度和高度的最大值和最小值。
    1
    2
    3
    Modifier.widthIn(2.dp) // 设置最大宽度 
    Modifier.heightIn(3.dp) // 设置最大高度
    Modifier.sizeIn(4.dp, 5.dp, 6.dp, 7.dp) // 设置最大最小的宽度和高度
    gravity
    在Column中元素的位置。
    1
    2
    3
    Modifier.gravity(Alignment.CenterHorizontally) // 横向居中 
    Modifier.gravity(Alignment.Start) // 横向居左
    Modifier.gravity(Alignment.End) // 横向居右
    rtl、ltr
    开始布局UI的方向。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Modifier.rtl // 从右到左 
    Modifier.ltr // 从左到右

    // Modifier的方法都返回Modifier的实例的链式调用,所以只要连续调用想要使用的方法即可。

    @Composable
    fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(20.dp).fillMaxSize())
    }

固有特性测量Modifier

Compose 有一项规则,即,子项只能测量一次,测量两次就会引发运行时异常。但是,有时需要先收集一些关于子项的信息,然后再测量子项。
借助固有特性,您可以先查询子项,然后再进行实际测量。
对于可组合项,您可以查询其 IntrinsicSize.Min 或 IntrinsicSize.Max

  • Modifier.width(IntrinsicSize.Min) - 需要多大的最小宽度才能正确显示内容?
  • Modifier.width(IntrinsicSize.Max) - 您需要多大的最大宽度才能正确显示内容?
  • Modifier.height(IntrinsicSize.Min) - 需要多高的最小高度才能正确显示内容?
    (在父组件加)可将其子项的高度强行调整为最小固有高度。
  • Modifier.height(IntrinsicSize.Max) - 您需要多高的最大高度才能正确显示内容?

线性布局Column,Row

Column 线性布局 ≈ Android LinearLayout-VERTICAL
Row 水平布局 ≈ Android LinearLayout-HORIZONTAL

ColumnRow可以理解为在View/Layout体系中的纵向和横向的ViewGroup

  • Modifier 用上述的方法传入已经按需求设置好的Modifier即可。
  • Arrangement.HorizontalArrangement.Vertical 需要给Row传入Arrangement.Horizontal,为Column传入Arrangement.Vertical。 这些值决定如何布置内部UI组件。
    可传入的值为Center, Start, End, SpaceEvenly, SpaceBetween, SpaceAround
  • Alignment.Vertical, Alignment.Horizontal 需要给Row传入Alignment.Vertical,为Column传入Alignment.Horizontal。 使用方法和Modifiergravity中传入参数的用法是一样的.
  • @Composable ColumnScope.() -> Unit 需要传入标有@Compose的UI方法。但是这里我们会有lamda函数的写法来实现。
    1
    2
    3
    4
    5
    Column { 
    Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically) {
    Text(text = "Hello $name!")
    }
    }

RowArrangement的值和对应效果如下:
Arrangement的值对应的效果

帧布局Box

帧布局 ≈ Android FrameLayout,可将一个元素放在另一个元素上,如需在 Row 中设置子项的位置,请设置 horizontalArrangementverticalAlignment 参数。对于 Column,请设置 verticalArrangementhorizontalAlignment 参数 。

寻呼机【分页器】HorizontalPager/VerticalPager

左右翻页:HorizontalPager(默认宽度满屏,默认一次翻一页,可配置) 
上下翻页:VerticalPager(默认高度满屏,默认一次翻一页,可配置) 
这些可组合项的功能与 View 系统中的 ViewPager 类似。

流式布局FlowRowFlowColumn

自定义版式

在界面树中布置每个节点的过程分为三个步骤。每个节点必须:

  1. 测量所有子项
  2. 确定自己的尺寸
  3. 放置其子项
    布置节点经历三个步骤

    [!注意]
    注意:Compose 界面不允许多遍测量。这意味着,布局元素不能为了尝试不同的测量配置而多次测量任何子元素。

自适应布局

Feed 布局能够以可配置网格的形式排列等效的内容元素,以便用户快速、方便地查看大量内容。

对齐线AlignmentLine

ConstraintLayout

在实现对齐要求比较复杂的较大布局时,很实用。
需要引入

1
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

在View系统中是建议用ConstraintLayout来创建复杂的大型布局,因为扁平视图

修饰符

修饰符的顺序

Slots API

TopAppBar

Scaffold

是个页面脚手架。可以为最常见的顶级 Material 组件(如 TopAppBarBottomAppBarFloatingActionButtonDrawer)提供槽位。通过使用 Scaffold,可轻松确保这些组件得到适当放置且正确地协同工作。

使用列表LazyColumn/LazyRow

  • 可以滚动的布局
    1
    2
    3
    4
    5
    6
    7
    8
    // 我们可以使用 verticalScroll() 修饰符使 Column 可滚动,但以上布局并无法实现重用,可能导致性能问题 
    Column(
    modifier = Modifier.verticalScroll(rememberScrollState())
    ) {
    messages.forEach { message ->
    MessageRow(message)
    }
    }
  • LazyColumn/LazyRow == RecylerView/listView 列表布局,解决了滚动时的性能问题,LazyColumn和LazyRow之间的区别就在于它们的列表项布局和滚动方向不同。
  • 内边距
    1
    2
    3
    4
    5
    LazyColumn( 
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
    ) {
    // ...
    }
  • item间距
    1
    2
    3
    4
    5
    LazyColumn( 
    verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
    // ...
    }
  • 浮动列表的浮动标题,使用 LazyColumn 实现粘性标题,可以使用stickyHeader()函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Composable
    fun ListWithHeader(items: List<Item>) {
    LazyColumn {
    stickyHeader {
    Header()
    }

    items(items) { item ->
    ItemRow(item)
    }
    }
    }
  • 网格布局LazyVerticalGrid
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Composable
    fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
    cells = GridCells.Adaptive(minSize = 128.dp)
    ) {
    items(photos) { photo ->
    PhotoItem(photo)
    }
    }
    }

自定义布局

官方:创建自定义布局创建自定义布局的Compose示例

通过重组基础布局实现

Canvas绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Composable
fun Greeting(name: String){
Column{
Row(modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically//设置垂直居中对齐
){
Text(text="Hello $name!")
}
}

//Canvas绘制
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
//drawCircle 画圆
//drawRectangle 画矩形
//drawLine //画线
drawCircle(
color = Color.DarkGray,
center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
radius = size.minDimension / 4
)
}
}

@Preview(showBackground=true,name="Text UI", backgroundColor=0xFF888888)
@Composable
private fun DefaultPreview(){
MyJetpackCOmposeTheme{
Greeting("Android")
}
}

firstBaselineToTop

MyOwnColumn

StaggeredGrid

约束布局

引用

约束条件

解耦API

Intrinsics

状态

官网:ViewModel和状态

什么是状态

与其它声明式 UI 框架一样,Compose 的职责非常单纯,仅作为对数据状态的反应。如果数据状态没有改变,则 UI 永远不会自行改变。在 Compose 中,每一个组件都是一个被 @Composable 修饰的函数,其状态就是函数的参数,当参数不变,则函数的输出就不会变,唯一的参数决定唯一输出。反言之,如果要让界面发生变化,则需要改变界面的状态,然后 Composable 响应这种变化。

Compose中的状态State

State

如同传统试图中,需要使用 StateFlow 或者 LiveData 将状态变量包装成一个可观察类型的对象。Compose 中也提供了可观察的状态类型,可变状态类型 MutableState 和 不可变状态类型 State。我们需要使用 State/MutableState 将状态变量包装起来,这样即可触发重组。更为方便的是,声明式 UI 框架中,不需要我们显式注册监听状态变化,框架自动实现了这一订阅关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Composable
fun CounterPage() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val counter: MutableState<Int> = mutableStateOf(0)
Log.d("sharpcj", "counter text --> ${counter.value}")
Text(text = "${counter.value}")
Button(onClick = {
Log.d("sharpcj", "increment button click ")
counter.value++
}) {
Text(text = "increment")
}
Button(onClick = {
Log.d("sharpcj", "decrement button click ")
counter.value--
}) {
Text(text = "decrement")
}
}
}

我们使用了 mutableStateOf() 方法初始化了一个 MutableState 类型的状态变量,并传入默认值 0 ,使用的时候,需要调用 counter.value
再次运行,结果发现,点击按钮,计数器值还是没有变化。

和上一次不一样了,这次发现,点击按钮之后, Text(text = "${counter.value}") 有重新执行,即发生了重组,但是执行的时候,参数没有改变,依然是 0,其实这里涉及到一个重组作用域的概念,就是重组是有一个范围的,关于重组作用范围,稍后再讲。这里需要知道,发生了重组,Text(text = "${counter.value}") 有重新执行,那么 val counter: MutableState<Int> = mutableStateOf(0) 也有重新执行,相当于重组时,counter 被重新初始化了,并赋予了默认值 0 。所以点击按钮发生了重组,但是计数器的值没有发生改变。要解决这个问题,则需要使用到 Compose 中的一个重要函数 remember

remember

remember 函数的源码:

1
2
3
4
5
6
7
/**
* Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
* Recomposition will always return the value produced by composition.
*/
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)

remember 方法的作用是,对其包裹起来的变量值进行缓存,后续发生重组过程中,不会重新初始化,而是直接从缓存中取。具体使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Composable
fun CounterPage() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val counter: MutableState<Int> = remember { mutableStateOf(0) }
Log.d("sharpcj", "counter text --> ${counter.value}")
Text(text = "${counter.value}")
Button(onClick = {
Log.d("sharpcj", "increment button click ")
counter.value++
}) {
Text(text = "increment")
}
Button(onClick = {
Log.d("sharpcj", "decrement button click ")
counter.value--
}) {
Text(text = "decrement")
}
}
}

再次运行,这次终于正常了。

上面的代码中,我们创建 State 的方法如下:

1
val counter: MutableState = remember { mutableStateOf(0) } 

使用时,通过 counter.value 来使用,这样的代码看起来就很繁琐,我们可以进一步精简写法。 首先, Kotlin 支持类型推导,所以可以写成下面这样:

1
val counter = remember { mutableStateOf(0) }

另外,借助于 Kotlin 委托语法,Compose 实现了委托方式赋值,使用 by 关键字即可,用法如下:

1
var counter by remember { mutableStateOf(0) } 

并导入如下方法:

1
2
import androidx.compose.runtime.getValue 
import androidx.compose.runtime.setValue

在使用时,直接使用 counter++counter--,需要注意的一点是,没有使用委托方式创建的对象,类型是 MutableState 类型,我们用 val 声明,使用委托方式创建对象,对象类型是 MutableState 包装的对象类型,这里由于赋初始值为 0 ,根据类型推导,counter 就是 Int 型,由于要修改 counter 的值,所以须使用 var 将其声明为一个可变类型对象。

rememberSaveable

1
var counter by rememberSaveable { mutableStateOf(0) }

用法与 remember 方法用法类似,区别在于,rememberSaveable 在当 Activity 销毁重建时,状态值不会重新初始化(如:横竖屏旋转,UiMode 切换等场景中),能够对其包裹的数据进行缓存。那是否说明 rememberSaveable 可以在所有的场景替换 remember , remember 方法就没用了? rememberSaveable 方法比 remember 方法功能更强劲,代价就是性能要差一些,具体使用根据实际场景来选择。

Parcelize
MapSaver
ListSaver

UI更新循环

非结构化状态

单一信息源(Single Source of Truth, SSOT)

在 Jetpack Compose 中,单一信息源的核心理念是:
UI 应完全由一个统一的状态驱动,而这个状态应该存放在一个明确的地方(通常是 ViewModel 或 remember 状态对象中)。

例如,一个按钮的点击次数应该只存储在一个地方,而不是分别在按钮和显示组件中重复存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//反例
@Composable
fun BrokenCounter() {
var count1 by remember { mutableStateOf(0) }
var count2 by remember { mutableStateOf(0) }

Column {
Button(onClick = { count1++ }) {
Text("Count1: $count1")
}
Button(onClick = { count2++ }) {
Text("Count2: $count2")
}
}
}


//改进
@Composable
fun CorrectCounter() {
var count by remember { mutableStateOf(0) }

Column {
Button(onClick = { count++ }) {
Text("Count: $count")
}
Button(onClick = { count = 0 }) {
Text("Reset Count")
}
}
}

好处:

  • 数据一致性
    状态存储在一个地方,UI 和逻辑依赖同一个数据来源,避免了因重复存储导致的数据冲突。
  • 更易调试
    如果状态有问题,只需要检查单一信息源,而不用逐一排查各个组件。
  • 降低复杂性
    数据集中管理,避免在多个地方维护相同的状态逻辑。
  • 增强可扩展性
    当需求增加时,你只需在单一信息源中修改或扩展逻辑,而不需要更新多个地方的状态代码。

单向数据流

单向数据流(UDF) 是一种数据流动模式,强调 “数据向下流动,事件向上传递” 。在 Compose 中,这意味着状态由父组件管理并传递给子组件,而子组件通过回调通知父组件更新状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//反例--状态由子组件直接管理
@Composable
fun ParentComponent() {
Column {
ChildComponent()
}
}

@Composable
fun ChildComponent() {
var text by remember { mutableStateOf("Initial Text") }
Column(
modifier = Modifier.padding(8.dp)
) {
TextField(
value = text,
onValueChange = {
text = it
},
label = { Text("Enter text") }
)
Button(onClick = { /* 使用 text 做一些逻辑操作 */ }) {
Text("Submit")
}
}
}



//改进--状态由父组件管理
@Composable
fun ParentComponent() {
var text by remember { mutableStateOf("Initial Text") }
Column {
ChildComponent(text = text, onTextChange = { newText -> text = newText })
Button(onClick = { /* 使用 text 做一些逻辑操作 */ }) {
Text("Submit")
}
}
}

@Composable
fun ChildComponent(text: String, onTextChange: (String) -> Unit) {
Column(
modifier = Modifier.padding(8.dp)
) {
TextField(
value = text,
onValueChange = onTextChange,
label = { Text("Enter text") }
)
}
}

好处:

  1. 逻辑清晰:状态只从一个地方流向 UI,事件只回到状态来源,这样的数据流向使得应用的逻辑非常直观。
  2. 易于维护:所有状态更新都集中管理,减少了因状态散落在多个地方而带来的调试难度。
  3. 组件的复用性更高:子组件无需管理状态,只负责展示数据,因而可以在多个场景中重复使用,而不需要关心不同的状态逻辑。

无状态组件

stateless 是指这个组件除了依赖参数以外,不依赖其他任何状态。比如 Text 组件

1
Text("Hello, Compose")

Stateless不依赖外部状态,仅依赖传入进来的参数,是一个“纯函数”,即唯一输入对应唯一输出。就是参数不变UI也不变,它的重组只能是来自上层的调用,因此 Compose 编译器对其进行了优化,当 Stateless 的参数没有变化时,它就不会参与重组,重组的范围局限于 Stateless 外部。另外 Stateless 不耦合任何业务,功能更纯粹,所以复用性更好,更易于测试。

有状态组件

stateful 除了依赖参数外还持有或访问了外部的状态。比如 TextField 组件

1
2
3
4
var text by remember { mutableStateOf("文本框初始值")}
TextField(value = text, onValueChange = {
text = it
})

状态提升

stateful 组件改造成 stateless 组件的过程称为状态上提。
通常做法:将内部状态移除,以参数的形式传入。需要回调给调用方的事件,也以参数形式传入
例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Composable
fun CounterPage() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var counter by remember{ mutableStateOf(0) }
Text(text = "$counter")
//计数器依赖了内部counter,两个按钮的点击事件会改变counter
Button(onClick = {
counter++
}) {
Text(text = "increment")
}
Button(onClick = {
counter--
}) {
Text(text = "decrement")
}
}
}



//改成
@Composable
fun CounterPage(counter: Int, onIncrement: () -> Unit, onDecrement: () -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "$counter")
Button(onClick = {
onIncrement()
}) {
Text(text = "increment")
}
Button(onClick = {
onDecrement()
}) {
Text(text = "decrement")
}
}
}

不适合使用状态提升:
例如,当状态只与特定子组件有关且不会被其他组件使用时,可以考虑将状态保留在子组件中。

状态管理

使用 stateful 管理状态

简单的的 UI 状态,且与业务无关的状态,适合在 Compose 中直接管理。
比如我有一个菜单列表,点一开关,展开一个菜单,再点一下,收起菜单,列表的状态,仅由点击开关这一单一事件决定。并且,列表的状态与任何外部业务无关。那么这种就适合在 Compose 内部进行管理。

使用 StateHolder 管理状态

当业务有一定的复杂度之后,我们可以将业务逻辑相关的状态统一封装到一个 StateHoler 进行管理。剥离 Ui 逻辑,让 Composable 专注 UI 布局。

使用 ViewModel 管理状态

从某种意义上讲,ViewModel 也是一种特殊的 StateHolde。但因为它是保存在 ViewModelStore 中,所以有以下特点:

  • 存活范围大,可以脱离 Composition 存在,被所有 Composable 共享。
  • 存活时间长,不会因为横竖屏切换或者 UiMode 切换导致数据丢失。

因此,ViewModel 适合管理应用程序全局状态,而且 ViewModel更倾向于管理哪些非 UI 的业务状态。
以上管理方式可以同时使用,结合具体的业务灵活搭配。

LiveData、Rxjava、Flow 转 State

在 MVVM 架构中,使用 ViewModel 来管理状态,如果是新项目,把状态直接定义 State 类型就可以了。
对于传统试图项目,一般使用 LiveData、Rxjava 或者 Flow 这类响应式数据框架。而在 Compose 中需要 State 触发重组,刷新 UI,也有相应的方法,将上述响应式数据流转换为 Compose 中的 State。当上有数据变化时,可以驱动 Composable 完成重组。具体方法如下:

拓展方法 依赖库
LiveData.observeAsState() androidx.compose:runtime-livedata
Flow.collectAsState() 不依赖三方库,Compose 自带
Observable.subscribeAsState() androidx.compose:runtime-rxjava2 或者 androidx.compose:runtime-rxjava3

重组

组件树

MutableState

ViewModel和状态

主题

Material Design

定义主题

使用颜色

处理文本

使用形状

CompositionLocal

简介

自定义CompositionLocal

替代方案

集成

从XML创建可组合

ViewModels & LiveData

Compose 中使用 View 控件

共用主题

手势

点击

滚动

滚动修饰符

可滚动的修饰符

嵌套滚动

拖动

滑动

多点触控

平移

缩放

旋转

动画

简单值动画

可见性动画

内容大小动画

多值动画

重复动画

手势动画

导航

集成导航

参数传递

深层链接

核心思想

编程思想

声明性编程范式

声明性的函数构建一个简单的界面组件,无需修改任何 XML 布局,也不需要使用布局编辑器,只需要调用 Jetpack Compose 函数来声明想要的元素,Compose 编译器即会完成后面的所有工作。

简单的可组合函数

1
2
3
4
@Composable 
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

声明性范式转变

在 Compose 的声明性方法中,微件相对无状态,并且不提供 setter 或 getter 函数。实际上,微件不会以对象形式提供。您可以通过调用带有不同参数的同一可组合函数来更新界面。这使得向架构模式(如 ViewModel)提供状态变得很容易,如应用架构指南中所述。然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。

动态内容

组合函数是用 Kotlin 而不是 XML 编写

重组

在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 – 系统会根据需要使用新数据重新绘制函数发出的微件。Compose 框架可以智能地仅重组已更改的组件。

  • 可组合函数可以按任何顺序执行
  • 可组合函数可以并行运行
  • 重组会跳过尽可能多的内容
  • 重组是乐观的操作
  • 可组合函数可能会非常频繁地运行

生命周期

生命周期概览

组合中可组合项的剖析

架构分层

运行时
界面
基础
Material

设计原则

语义

语义属性

合并和未合并的语义树

检查树
合并行为

调整语义树

附带效应

LaunchedEffect

rememberCoroutineScope

rememberUpdatedState

DisposableEffect

SideEffect

produceState

DerivedStateOf

snapshotFlow

重启效应