Jetpack Compose 是用于构建原生 Android 界面的新工具包。 它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发。 在本教程中,您将使用声明性的函数构建一个简单的界面组件。 您无需修改任何 XML 布局,也不需要使用布局编辑器。 相反,您只需调用可组合函数来定义所需的元素,Compose 编译器即会完成后面的所有工作。

1.入门

1.1.Composable

@Composable

声明为组件

1
2
3
4
5
6
7
8
9
10
@Composable
fun MessageBar(msg: String){
Text(text = msg)
}

@Preview
@Composable
fun PreViewText(){
MessageBar("Context")
}

1.2.Material Design

1.Theme.kt

在ui/theme/theme.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private val DarkColorScheme = darkColorScheme( //暗色主题
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
background = Color.Gray,
)

private val LightColorScheme = lightColorScheme( //亮色主题
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
background = Color.White,
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)

主题的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}

darkTheme -> DarkColorScheme
else -> LightColorScheme
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

2.使用主题

MaterialTheme.colorScheme.background

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Row (Modifier.padding(8.dp).
background(MaterialTheme.colorScheme.background//使用主题
)) {
Image(
painter = painterResource(id = R.drawable.test),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Column(Modifier.padding(all = 8.dp)) {
Text(text = msg)
}
}

1.3.列表与状态

1
2
3
4
5
6
7
8
@Composable
fun Conversations(messages:List<String>){
LazyColumn {
items(messages) {message ->
MessageBar(msg = message)
}
}
}

调用

1
2
val conversations = listOf("1","2","3")
Conversations(conversations)

点击Column改变变量值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var isExpand by remember {
mutableStateOf(false)
} //持久化存储该变量
val mcolor:Color by animateColorAsState(
targetValue = if (isExpand) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surface)
//点击按钮改变颜色
Column(
Modifier
.padding(all = 8.dp)
.clickable { isExpand = !isExpand }) {

Surface (color = mcolor){ //添加surface组件
Text(text = "hello and $msg",
style = TextStyle(Color.Blue),
maxLines = if(isExpand) Int.MAX_VALUE else 1)
}
}

1.4.列表

通过repeat(100)创建100个列表项

items(100)也可以达到同等效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun Loop(){
val scrollState = rememberScrollState()
Column(modifier = Modifier.verticalScroll(scrollState)) {
repeat(100){
Text(text = "$it times", style = MaterialTheme.typography.titleMedium)
}
}
}

@Composable
fun LazyLoop(){
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState){
items(100){
Text(text = "$it times", style = MaterialTheme.typography.titleMedium)
}
}
}

协程的使用

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
@Composable
fun ScrollList(){
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val listSize = 100;

Column {
Row {
Button(modifier = Modifier.weight(1f),onClick = {
//启动协程
coroutineScope.launch {
scrollState.animateScrollToItem(0) //播放动画,跳转到列表头部
}
}) {
Text(text = "top")
}
Button(modifier = Modifier.weight(1f),onClick = {
//启动协程
coroutineScope.launch {
scrollState.animateScrollToItem(listSize -1) //播放动画,跳转到列表末尾
}
}) {
Text(text = "end")
}
}

LazyColumn(state = scrollState){
items(listSize){
ImageItem(it)
}
}
}
}

@Composable
fun ImageItem(Index:Int){
Row (verticalAlignment = Alignment.CenterVertically){
Image(painter = painterResource(id = R.drawable.test), contentDescription = null)
Text(text = "item $Index", style = MaterialTheme.typography.titleMedium)
}
}

1.5自定义仿修饰符

fun Modifier.xxxx = this.then()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun Modifier.firstToTop(BottomToTop: Dp) = this.then(
layout{measurable, constraints ->
//测量物体
val placeable = measurable.measure(constraints)
//获取物体的FirstBaseline
val firstBaseLine = placeable[FirstBaseline]
//测量得到长度
val placeableY = BottomToTop.roundToPx() - firstBaseLine
//设置物体
layout(placeable.width,placeable.height + placeableY){
placeable.placeRelative(0,placeableY)
}
}
)

@Composable
fun TestModifier(){
Text(text = "heffff", modifier = Modifier.firstToTop(100.dp).background(color = MaterialTheme.colorScheme.surface))
}

1.6自定义组件

使用Layout设置组件布局

通过layout设置每个物体的位置

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
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
){
Layout(content = content, modifier = modifier){measurables, constraints ->
//测出每个物体的数据
val placeable = measurables.map{measurable->
measurable.measure(constraints)
}

var yPosition = 0
//将高度设置为父物体的最大高度和最低高度
layout(constraints.maxWidth,constraints.maxHeight){
//设置每个物体的位置
placeable.forEach{
it.placeRelative(0,yPosition)
yPosition += it.height
}
}
}
}

@Composable
fun MyOwnColumnTest(){
MyOwnColumn(modifier = Modifier.padding(8.dp)) {
Text(text = "test")
Text(text = "test")
}
}

2.进阶使用

2.1constraintlayout

在build.gradle.kt中添加依赖

1
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.1")

或在build.gradle中添加

1
implementation 'androidx.constraintlayout:constraintlayout-compose:1.1.1'

使用约束布局

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
@Composable
fun MyConstraintLayout() {
ConstraintLayout{
val (button,text) = createRefs()
Button(onClick = {}, modifier = Modifier.constrainAs(button){
top.linkTo(parent.top, margin = 16.dp)
}) {
Text(text = "button")
}
//将文本组件限制在按钮的底部
Text(text = "Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
centerHorizontallyTo(parent)
})
}
}

@Composable
fun MyTowConsLayout(){
ConstraintLayout {
val (buttono,buttont,text) = createRefs()

Button(onClick = { }, modifier = Modifier.constrainAs(buttono){
top.linkTo(parent.top, margin = 16.dp)
}) {
Text(text = "Button o")
}

Text(text = "text", modifier = Modifier.constrainAs(text){
top.linkTo(buttono.bottom, margin = 16.dp)
centerAround(buttono.end)
})

//将buttono和text组合成一个屏障
val barrier = createEndBarrier(buttono,text)
Button(onClick = {}, modifier = Modifier.constrainAs(buttont) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(barrier)
}) {
Text(text = "Button t")
}
}
}

长文本设置换行

1
2
3
4
5
6
7
8
9
10
11
@Composable
fun LargeConsLayout(){
ConstraintLayout {
val text = createRef()

val guildline = createGuidelineFromStart(fraction = 0.5f)
Text(text = "this is a very very very very very very very very very long text", modifier = Modifier.constrainAs(text) {
linkTo(start = guildline,end = parent.end)
width = Dimension.preferredWrapContent //换行
})
}

2.2解耦API

将约束定义在另一个函数,通过解耦API获得约束

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
@Composable
fun Deconple2(){
BoxWithConstraints {
//判断横屏还是竖屏
val constraints = if (maxWidth < maxHeight){
decouple(16.dp) //竖屏
} else {
decouple(160.dp) //横屏
}
//使用约束
ConstraintLayout(constraints) {
Button(onClick = {}, modifier = Modifier.layoutId("button")) {
Text(text = "button")
}
//将文本组件限制在按钮的底部
Text(text = "Text", Modifier.layoutId("text"))
}
}

}

private fun decouple(margin: Dp) : ConstraintSet{
return ConstraintSet{
val button = createRefFor("button")
val text = createRefFor("text")

constrain(button){
top.linkTo(parent.top, margin)
}

constrain(text){
top.linkTo(button.bottom, margin)
}
}
}

2.3Intrinsics

获取子元素的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun TwoTexts(modifier: Modifier = Modifier){
//高度为最小值
Row (modifier = Modifier.height(IntrinsicSize.Min)){
Text(text = "Hi",modifier = Modifier
.padding(1.dp)
.weight(1f)
.wrapContentWidth(Alignment.Start))
//分割线
Divider(color = Color.Black, modifier = Modifier
.fillMaxHeight()
.width(1.dp))

Text(text = "Android",modifier = Modifier
.padding(1.dp)
.weight(1f)
.wrapContentWidth(Alignment.End))
}
}

实现效果

20250307ea780202503071104106999.png

3.compose状态

3.1无状态组件

TodoItems

1
2
3
4
5
6
7
8
9
10
11
data class ToDoItems(val task:String,val icon:TodoIcon = TodoIcon.default,val id:UUID = UUID.randomUUID())

enum class TodoIcon(
val imageVector:ImageVector,
val desc:String
){
SQU(Icons.Default.Favorite,"喜欢");
companion object {
val default = SQU
}
}

组件

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
@Composable
fun TwoTexts(modifier: Modifier = Modifier){
//高度为最小值
Row (modifier = Modifier.height(IntrinsicSize.Min)){
Text(text = "Hi",modifier = Modifier
.padding(1.dp)
.weight(1f)
.wrapContentWidth(Alignment.Start))
//分割线
Divider(color = Color.Black, modifier = Modifier
.fillMaxHeight()
.width(1.dp))

Text(text = "Android",modifier = Modifier
.padding(1.dp)
.weight(1f)
.wrapContentWidth(Alignment.End))
}
}

@Composable
fun TodoSreen(items:List<ToDoItems>){
Column{
LazyColumn(modifier = Modifier.weight(1f), contentPadding = PaddingValues(10.dp)) {
items(items){
TodoRow(it, modifier = Modifier.fillParentMaxWidth())
}
}
Button(onClick = {}, modifier = Modifier.padding(16.dp).fillMaxWidth()) {
Text(text = "click me!!!")
}
}
}

@Composable
fun TodoRow(
todo:ToDoItems,
modifier: Modifier = Modifier,
//content:@Composable ()->Unit
){
Row (modifier = modifier.padding(vertical = 6.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween //子元素水平布局
){
Text(text = todo.task)

Icon(imageVector = todo.icon.imageVector, contentDescription = todo.icon.desc)
}

}

3.2非结构化状态

在gradle.build.kt文件中添加

1
2
3
4
buildFeatures {
compose = true
viewBinding = true
}

新建activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TestCom :AppCompatActivity(){

private val bindding by lazy {
ActivityTestComBinding.inflate(layoutInflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(bindding.root)

bindding.input.doAfterTextChanged{ text ->
uploadText(text.toString())
}
}

private fun uploadText(text: String) {
bindding.text.text = text
}
}

使用ViewModel

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
class TestViewModel:ViewModel(){
private val _name = MutableLiveData("")
val name: LiveData<String> = _name

fun onNameChanged(newname:String){
_name.value = newname
}
}

class TestCom :AppCompatActivity(){

private val testmodel by viewModels<TestViewModel>();

private val bindding by lazy {
ActivityTestComBinding.inflate(layoutInflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(bindding.root)

bindding.input.doAfterTextChanged{ text ->
testmodel.onNameChanged(text.toString())
}

testmodel.name.observe(this){
text->
bindding.text.text = text
}
}
}

3.3状态与状态提升

将TodoItemList作为状管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TodoItemModel : ViewModel(){
private var _todos = MutableLiveData(listOf<ToDoItems>())

val todos : LiveData<List<ToDoItems>> = _todos

//添加item
fun AddItem(todo:ToDoItems){
_todos.value = _todos.value!! + listOf(todo)
}

//删除item
fun DeleteItem(todo:ToDoItems){
_todos.value = _todos.value!!.toMutableList().also {
it.remove(todo)
}
}
}

构建主界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private val todoModel by viewModels<TodoItemModel>();

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

setContent {
val todolist:List<ToDoItems> by todoModel.todos.observeAsState(listOf())
Scaffold{
Column (modifier = Modifier.padding(it)){
//将todolist和 添加item 删除item传递到下一级组件
TodoSreen(todolist, ItemAdd = {todoModel.AddItem(it)}, ItemDelete = {todoModel.DeleteItem(it)})
}
}
}
}

3.4remember

1. 保留状态

当你需要在 Composable 函数中保存某个状态,并且希望在组件重组时保留这个状态,可以使用 remember

1
2
3
4
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}

在这个例子中,count 的值在按钮点击时会增加,并且在组件重组时,count 的值会被保留。

2.避免重复计算

如果你有一个复杂的计算结果,不希望在每次重组时重新计算,可以使用 remember 来缓存计算结果。

1
2
3
val complexResult = remember {
computeComplexResult() // 假设这是一个耗时的计算函数
}

3. 带条件的 remember

你可以根据某些条件来决定是否重新计算值。remember 接收可变参数 keys,当这些 keys 的值发生变化时,remember 会重新计算并保存新的值。

1
2
3
4
var text by remember { mutableStateOf("") }
val reversedText = remember(text) {
text.reversed()
}

在这个例子中,reversedText 会在 text 发生变化时重新计算。

4. 结合 mutableStateOf 使用

remember 常与 mutableStateOf 一起使用,用于创建可变状态。

1
2
3
4
var isExpanded by remember { mutableStateOf(false) }
Button(onClick = { isExpanded = !isExpanded }) {
Text("Expand: $isExpanded")
}

5. 保存多个值

remember 可以保存多个值,或者保存一个包含多个值的对象。

1
2
3
4
5
data class User(val name: String, val age: Int)

val user = remember {
User("Alice", 30)
}

6. 与 ViewModel 结合使用

在 Jetpack Compose 中,remember 也可以与 ViewModel 结合使用,保存与 UI 相关的状态。

1
2
3
4
5
val viewModel = viewModel<MyViewModel>()
var data by remember { mutableStateOf(emptyList<String>()) }
LaunchedEffect(Unit) {
data = viewModel.loadData()
}

7. rememberSaveable

如果你需要在配置更改(如屏幕旋转)时保留状态,可以使用 rememberSaveable,它会在配置更改时自动保存和恢复状态。

1
2
3
4
var count by rememberSaveable { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}

3.5mutableStateOf

  1. 创建可组合函数

    1
    2
    3
    4
    @Composable
    fun StateExample() {

    }
  2. 创建可观察的状态

    1
    2
    3
    val (count,setCount) = remember {
    mutableStateOf(0)
    }
    • mutableStateOf(0):创建一个初始值为 0 的可观察状态。
    • remember:确保在组件重新绘制时,状态不会被重置。
    • by:使用委托语法,使 count 变成一个可读写的属性。
  3. 创建按钮并处理点击事件

    1
    2
    3
    Button(onClick = { count++ }) {
    Text("Clicked $count times")
    }
    • Button:一个可组合的按钮组件。
    • onClick:点击按钮时的回调函数,这里增加 count 的值。

工作原理

  1. 状态创建
    • mutableStateOf 创建一个 MutableState 对象,内部包含一个值和一些元数据。
    • remember 确保在组件重新绘制时,状态不会被重置。
  2. 状态观察
    • count 被使用时,Compose 运行时会自动将当前可组合函数注册为 count 的观察者。
  3. 状态更新
    • count 的值发生变化时(如点击按钮时 count++),Compose 运行时会检测到状态变化。
  4. 触发重组
    • 在下一个组合周期,Compose 运行时会重新执行使用了 count 的可组合函数,更新 UI。

注意事项

  • 状态保存
    • mutableStateOf 本身并不保存状态,它只是创建一个可观察的状态对象。如果需要在组件重新绘制时保持状态,通常需要结合 remember 函数一起使用。
  • 状态变化检测
    • Compose 的状态变化检测是基于引用的。对于复杂对象,如果只是修改了对象的内部属性,而没有改变对象本身,Compose 可能不会检测到状态的变化。这种情况下,可能需要手动通知状态变化。
  • 性能优化
    • 虽然 mutableStateOf 提供了强大的状态管理能力,但在实际开发中,应尽量减少不必要的状态更新,以提高应用的性能。

3.6输入框

小项目实战

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//输入框
@Composable
fun InputEdit(
text:String,
modifier: Modifier = Modifier,
onTextChange:(String)->Unit
){
TextField(onValueChange = onTextChange,
maxLines = 1,
value = text,
modifier = modifier,
colors = TextFieldDefaults.colors(unfocusedTextColor = Color.Magenta))
}

//按钮
@Composable
fun InputButton(
onClick:()->Unit,
text: String,
modifier: Modifier = Modifier,
enable : Boolean = true
){
TextButton(onClick = onClick, enabled = enable, modifier = modifier,
shape = CircleShape,
colors = ButtonDefaults.buttonColors()) {
Text(text = text)
}
}


@Composable
fun AnimateIconRow(
icon:TodoIcon,
OnIconChange:(TodoIcon)->Unit,
modifier: Modifier = Modifier,
visiable:Boolean = true
){
val enter = remember {
fadeIn(animationSpec = TweenSpec(200000, easing = FastOutSlowInEasing))
}
val exit = remember {
fadeOut(animationSpec = TweenSpec(200000, easing = FastOutSlowInEasing))
}
Box(modifier = modifier.defaultMinSize(minHeight = 16.dp)){
AnimatedVisibility(visiable,enter = enter , exit = exit) {
IconRow(icon = icon,OnIconChange = OnIconChange,modifier = modifier)
}
}
}

@Composable
fun SelectableButton(
modifier:Modifier,
icon: ImageVector,
desc: String,
OnIconSelect: () -> Unit,
isSelected: Boolean
) {
val tint = if (isSelected) Color.Magenta else MaterialTheme.colorScheme.surface.copy(0.6f)

TextButton(onClick = {OnIconSelect()}, shape = CircleShape, modifier = modifier) {
Column {
Icon(imageVector = icon, contentDescription = desc)
if (isSelected){
Box (modifier = Modifier
.padding(top = 3.dp)
.height(1.dp)
.width(icon.defaultWidth)
.background(color = tint)){
}
}else{
Spacer(modifier = Modifier.height(4.dp))
}
}
}
}


@Composable
fun IconRow(icon: TodoIcon, OnIconChange: (TodoIcon) -> Unit,modifier : Modifier = Modifier) {
Row (modifier) {
for (todoicon in TodoIcon.entries){
SelectableButton(
modifier = Modifier,
icon = todoicon.imageVector,
desc = todoicon.desc,
OnIconSelect = {OnIconChange(todoicon)},
isSelected = todoicon == icon
)
}
}
}

@Composable
fun InputList(){
//记录输入的值
val (text,setText) = remember {
mutableStateOf("test")
}
//记录标记的icon
val (icon,setIcon) = remember {
mutableStateOf(TodoIcon.default)
}
val iconVisable = text.isNotBlank()
Column {
Row (modifier = Modifier.padding(10.dp)){
InputEdit(
text = text, onTextChange = setText,
modifier = Modifier
.padding(end = 8.dp)
)

InputButton(onClick = {} ,
enable = text.isNotBlank(),
text = "提交",
modifier = Modifier
.align(Alignment.CenterVertically)
.fillMaxWidth())
}
//判断是否可以显示IconRow
if (iconVisable){
AnimateIconRow(icon,setIcon, modifier = Modifier.padding(8.dp))
}
}
}

3.7状态恢复

1.Parcelize

在 Kotlin 中,@Parcelize 是一个注解,用于简化实现 Parcelable 接口的过程。Parcelable 是 Android 中用于序列化对象的接口,以便对象可以在不同组件之间传递,例如在 Intent 中传递或者保存在 Bundle 中。

使用步骤

  1. 添加插件
    在项目的 build.gradle.kt 文件中添加 kotlin-parcelize 插件:

    1
    2
    3
    plugins {
    id("kotlin-parcelize")
    }
  2. 注解数据类
    使用 @Parcelize 注解你的数据类,并让它继承 Parcelable 接口:

    1
    2
    @Parcelize
    data class City(val name: String, val country: String) : Parcelable
  3. 使用对象
    现在,你可以在需要传递对象的地方使用这个数据类,例如在 Intent 中:

    1
    2
    3
    val intent = Intent(this, AnotherActivity::class.java)
    intent.putExtra("city", City("Beijing", "China"))
    startActivity(intent)

    或者在 rememberSaveable 中保存状态:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Composable
    fun CityScreen() {
    var city by rememberSaveable { mutableStateOf(City("London", "UK")) }
    Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp)) {
    TextButton(onClick = { city = City("Beijing", "CHINA") }) {
    Text(text = "Click to change")
    }
    Text(text = "${city.country}-${city.name}")
    }
    }

注意事项

  • 所有属性必须可序列化@Parcelize 注解的数据类的所有属性必须是可序列化的,即它们本身要么是基本类型,要么也是实现了 ParcelableSerializable 的类。
  • **自定义 Saver**:如果 @Parcelize 不适合你的场景,你还可以使用 mapSaverlistSaver 自定义存储规则。

通过这种方式,你可以轻松地在 Android 应用中实现对象的序列化和传递。

mapSaver的作用

  • 定义保存规则:mapSaver用于定义如何将对象转换为Map<String, Any>的结构进行保存。它允许开发者自定义保存和恢复的规则,通过指定键值对来存储对象的属性。

  • 保存复杂对象:当对象的属性较多或结构复杂时,使用mapSaver可以更清晰地指定每个属性的保存方式。

  • 示例代码

    kotlin复制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    data class City(val name: String, val country: String)

    val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
    save = { mapOf(nameKey to it.name, countryKey to it.country) },
    restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
    }

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

listSaver的作用

  • 简化保存过程:listSaver用于将对象转换为List的数据结构进行保存。它使用列表的索引作为键,避免了为映射定义键的麻烦。

  • 适用于简单对象:对于简单的对象结构,使用listSaver可以更方便地进行状态保存和恢复。

  • 示例代码

    kotlin复制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    data class City(val name: String, val country: String)

    val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
    )

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

mapSaver和listSaver的区别

  • 数据结构不同:mapSaver将对象转换为Map结构,而listSaver将对象转换为List结构。
  • 使用场景不同:当需要明确的键来标识属性时,mapSaver更合适;而当数据结构简单且顺序固定时,listSaver更方便。
  • 灵活性不同:mapSaver在处理复杂对象时更灵活,因为它允许开发者自定义每个属性的键;而listSaver在处理简单对象时更简洁。
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
	//指定mapSaver,用于在存储的时候如何构建一个Map对象,获取时如何构建一个city对象
val citySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(save = { mapOf(nameKey to it.name,countryKey to it.country)},
restore = {MainActivity.OCity(it[nameKey] as String,it[countryKey] as String)})
}

//指定listSaver,用于在存储的时候构建一个List对象,读取时如何构建一个city对象
val citySavert = listSaver(
save = { listOf(it.name,it.country)},
restore = {MainActivity.OCity(it[0] as String,it[1] as String)}
)

//可以在屏幕选转之后保存数据
val (city,setCity) = rememberSaveable(stateSaver = citySavert) {
mutableStateOf(MainActivity.OCity("Madrid","Spain"))
}

/* val (citys,setCitys) = remember {
mutableStateOf(MainActivity.OCity("Madrid","Spain"))
}*/

Row (verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp)){
TextButton(onClick = { setCity(MainActivity.OCity("Beijing","China")) }) {
Text(text = "Change")
}
Text(text = city.toString())
}

3.8CompositionLocalProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
MaterialTheme {
Column {
Text(text = "1")
CompositionLocalProvider(LocalContentColor provides LocalContentColor.current.copy(alpha = 0.6f)) {
Text(text = "2")
Text(text = "3")
}
CompositionLocalProvider(LocalContentColor provides LocalContentColor.current.copy(alpha = 0.38f)) {
Descendant()
}
FruitTitle(size = 1)
}
}

string.xml文件

1
2
3
4
5
6
7
<resources>
<string name="app_name">Kcom</string>
<plurals name="fruit_title">
<item quantity="one">fruit</item>
<item quantity="other">fruits</item>
</plurals>
</resources>

如果size为1则匹配one

size为2,3….则匹配other

1
2
3
4
5
6
7
8
9
FruitTitle(size = 1

@Composable
fun FruitTitle(size:Int){
val resource = LocalContext.current.resources

val fruit = resource.getQuantityString(R.plurals.fruit_title,size)
Text(text = fruit)
}

自定义compositionLocal

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
//定义数据类
data class Elevations(val cradDp: Dp = 0.dp)

//定义CompositionLocal
val LocalElevations = compositionLocalOf {
Elevations()
}

//定义CardElevation
object CardElevation{
val high:Elevations
get() = Elevations(10.dp)
val low:Elevations
get() = Elevations(5.dp)
}

@Composable
fun Test(){
Column {
CompositionLocalProvider(LocalElevations provides CardElevation.low) {
MyCard(background = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f)) {
}
}

MyCard(background = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)) {
}
}
}


@Composable
fun MyCard(
elevation: Dp = LocalElevations.current.cradDp,
background: Color,
content: @Composable ColumnScope.()->Unit
){
Card(elevation = CardDefaults.cardElevation(defaultElevation = elevation),modifier = Modifier.size(200.dp), colors = CardDefaults.cardColors(containerColor = background), content = content)
}

4.主题

1.定义主题

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
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
background = Color(0xFFFFFBFE),
)

private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),

)

@Composable
fun NrcTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

darkTheme -> DarkColorScheme
else -> LightColorScheme
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

2.字体的使用

1
2
3
4
5
6
7
//一段文字有不同的大小
val text = buildAnnotatedString {
append("text") //添加一段文本
withStyle(MaterialTheme.typography.titleMedium.toSpanStyle()){
append("this is a test") //使用样式添加一段文本
}
}

5.动画

1.简单值动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Composable
fun MainWin(){
val (num,setNum) = remember {
mutableIntStateOf(0)
}
//设置背景动画
val backgroundColor by animateColorAsState(if(num == 0) Purple80 else Color.Green,label = "")

NrcTheme {
Scaffold(modifier = Modifier.fillMaxSize(), containerColor = backgroundColor) { innerPadding ->
Row(modifier = Modifier.padding(innerPadding)) {
Button(modifier = Modifier.weight(1f),onClick = {setNum(0)}) {
Text(text = "HomePage")
}
Button(modifier = Modifier.weight(1f),onClick = {setNum(1)}) {
Text(text = "TabBar")
}
}
}
}
}

2.AnimatedVisibility

AnimatedVisibility 是 Jetpack Compose 中的一个组件,用于在组件的可见性发生变化时添加进入和退出动画。它的主要作用是通过动画效果增强用户体验,使界面的变化更加流畅和自然。

具体作用

  1. 控制组件的可见性
    • 通过 visiblevisibleState 参数,可以动态控制子组件是否显示。
    • visibletrue 时,子组件显示;为 false 时,子组件隐藏。
  2. 添加进入动画
    • 当组件从不可见到可见时,可以通过 enter 参数定义进入动画。
    • 常见的进入动画包括 fadeIn(淡入)、slideIn(滑入)等。
  3. 添加退出动画
    • 当组件从可见到不可见时,可以通过 exit 参数定义退出动画。
    • 常见的退出动画包括 fadeOut(淡出)、slideOut(滑出)等。
  4. 灵活组合动画效果
    • 可以将多种动画效果组合使用,例如同时使用 fadeInslideInVertically

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var isVisible by remember { mutableStateOf(false) }

Button(onClick = { isVisible = !isVisible }) {
Text("Toggle Visibility")
}

AnimatedVisibility(
visible = isVisible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
Text("Hello, Compose!", modifier = Modifier.padding(16.dp))
}

通过animateContentSize可实现内容大小变化时的动画

1
modifier = Modifier.padding(innerPadding).animateContentSize()

3.多值动画

1. 使用 updateTransition 创建转场效果

updateTransition 根据状态变化创建转场,返回的 Transition 对象可用来声明动画值。

1
2
3
4
5
6
7
8
private sealed class BoxState(val color: Color, val size: Dp) {
operator fun not() = if (this is Small) Large else Small
object Small : BoxState(Blue, 64.dp)
object Large : BoxState(Red, 128.dp)
}

var boxState: BoxState by remember { mutableStateOf(BoxState.Small) }
val transition = updateTransition(targetState = boxState)

2. 针对每个属性定义动画

使用 Transition 对象上的扩展函数(如 animateRectanimateDp 等)为每个属性定义动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//定义颜色动画
val color by transition.animateColor(label = "Transition Tab1"){ it->
it.color //targetValue
}
//定义大小动画
val size by transition.animateDp(transitionSpec = {
if (targetState == BoxState.Large) {
spring(stiffness = Spring.StiffnessVeryLow)
} else {
spring(stiffness = Spring.StiffnessHigh)
}
}, label = "Transition Tab2") {it ->
it.size //targetValue
}

3. 在可组合项中应用动画值

在可组合项中使用动画值,使界面随状态变化而平滑过渡。

1
2
3
4
5
6
7
8
9
10
11
Button(
onClick = { boxState = !boxState }
) {
Text("Change Color and size")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.size(size)
.background(color)
)

4. 自定义动画行为

通过 transitionSpec 参数自定义动画行为,如动画时长、缓动函数等。

1
2
3
4
5
6
7
8
9
val size by transition.animateDp(transitionSpec = {
if (targetState == BoxState.Large) {
spring(stiffness = Spring.StiffnessVeryLow)
} else {
spring(stiffness = Spring.StiffnessHigh)
}
}, label = "Transition Tab2") {it ->
it.size
}

示例:多值动画完整代码

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
private sealed class BoxState(val color: Color, val size: Dp) {
operator fun not() = if (this is Small) Large else Small
object Small : BoxState(Blue, 64.dp)
object Large : BoxState(Red, 128.dp)
}

@Composable
fun UpdateTransitionDemo() {

var boxState: BoxState by remember { mutableStateOf(BoxState.Small) }
val transition = updateTransition(targetState = boxState)

Column(Modifier.padding(16.dp)) {
Spacer(Modifier.height(16.dp))

val color by transition.animateColor(label = "Transition Tab1"){ it->
it.color
}
val size by transition.animateDp(transitionSpec = {
if (targetState == BoxState.Large) {
spring(stiffness = Spring.StiffnessVeryLow)
} else {
spring(stiffness = Spring.StiffnessHigh)
}
}, label = "Transition Tab2") {it ->
it.size
}

Button(
onClick = { boxState = !boxState }
) {
Text("Change Color and size")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.size(size)
.background(color)
)
}
}

4.重复动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//使用rememberInfiniteTransition
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")

// 透明度动画
val alpha by infiniteTransition.animateFloat(
initialValue = 0f, //初始值
targetValue = 1f, //最终值
animationSpec = infiniteRepeatable( //重复动画
animation = tween(1000, easing = LinearOutSlowInEasing),
repeatMode = RepeatMode.Reverse
), label = "Test"
)
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.alpha(alpha) // 应用透明度动画
) {
Text(
text = "Multi-Property Animation",
modifier = Modifier.align(Alignment.Center)
)
}

5.手势动画

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
54
55
56
57
58
59
60
61
62
63
64
65
val list = mutableStateListOf(1,2,3,4)

@Composable
fun MainWIn() {
val onRemove:(Int)->Unit = { a ->
list.remove(a)
}
Column {
for (index in list){
Box(modifier = Modifier.swipe(ondismiss = { onRemove(index) }).height(100.dp).fillMaxWidth().padding(start = 10.dp, top = 10.dp, end = 10.dp).background(color = Color.Green)){
Text(text = "this is $index")
}
}
}
}

private fun Modifier.swipe(
ondismiss: () -> Unit
):Modifier = composed{

//创建动画
val offsetX = remember {
Animatable(0f)
}

pointerInput(Unit){
//创建一个基于样条函数的减速计算对象,用于计算滑动结束时的减速效果。
val decay = splineBasedDecay<Float>(this)

coroutineScope {
while (true){
//等待触摸事件
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
//记录滑动的速度
val velocityTracker = VelocityTracker()
//等待拖动事件
awaitPointerEventScope {
horizontalDrag(pointerId){ change->
//水平偏移量
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
offsetX.snapTo(horizontalDragOffset)
}
velocityTracker.addPosition(change.uptimeMillis,change.position)
}
}
//计算速度
val velocity = velocityTracker.calculateVelocity().x
//计算最终的位置
val targetPosition = decay.calculateTargetValue(offsetX.value,velocity)
launch {
if (targetPosition.absoluteValue <= size.width){
//如果距离不够 则按原速度滑回路
offsetX.animateTo(0f, initialVelocity = velocity)
}else{
offsetX.animateDecay(velocity,decay)
ondismiss()
}
}
}
}
}.offset {
IntOffset(offsetX.value.roundToInt(),0)
}
}
  1. 函数定义
1
2
3
private fun Modifier.swipe(
ondismiss: () -> Unit
): Modifier = composed { ... }
  • 定义了一个私有的 Modifier 扩展函数 swipe,接收一个回调函数 ondismiss
  • 使用 composed 构建一个可组合的 Modifier,以便在其中使用可组合函数和状态。

2. 动画状态初始化

1
2
3
val offsetX = remember {
Animatable(0f)
}
  • 使用 remember 创建一个可动画化的状态 offsetX,初始值为 0,用于跟踪水平偏移量。

3. 触摸事件处理

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
pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch { offsetX.snapTo(horizontalDragOffset) }
velocityTracker.addPosition(change.uptimeMillis, change.position)
}
}
val velocity = velocityTracker.calculateVelocity().x
val targetPosition = decay.calculateTargetValue(offsetX.value, velocity)
launch {
if (targetPosition.absoluteValue <= size.width) {
offsetX.animateTo(0f, initialVelocity = velocity)
} else {
offsetX.animateDecay(velocity, decay)
ondismiss()
}
}
}
}
}
  • **pointerInput**:处理触摸事件。
  • **splineBasedDecay**:创建一个基于样条函数的减速计算对象,用于计算滑动结束时的减速效果。
  • **coroutineScope**:启动一个协程作用域,用于处理异步任务。
  • **while (true)**:无限循环,持续监听触摸事件。
  • **awaitFirstDown**:等待第一个触摸点按下,获取其 pointerId
  • **velocityTracker**:记录滑动速度。
  • **horizontalDrag**:监听水平拖动事件,更新 offsetX 的值,并记录滑动位置和时间。
  • **calculateVelocity**:计算滑动速度。
  • **decay.calculateTargetValue**:根据当前偏移量和速度,计算滑动结束时的目标位置。
  • 动画逻辑
    • 如果滑动距离小于屏幕宽度,则动画回到初始位置。
    • 否则,执行减速动画并调用 ondismiss 回调。

4. 应用偏移量

1
2
3
.offset {
IntOffset(offsetX.value.roundToInt(), 0)
}
  • 使用 offset 修改器,根据 offsetX 的值对组件进行水平偏移。

6.手势

1.点击手势

1
2
3
4
5
6
7
8
Modifier.pointerInput(Unit) { 
detectTapGestures(
onTap = {},
onPress = {},
onDoubleTap = {},
onLongPress = {}
)
}
  1. onTap
1
onTap = {}
  • 作用:处理单击事件。
  • 触发条件:当用户在组件上快速点击一次时触发。
  • 适用场景:常用于实现按钮点击、选择项等功能。
  1. onDoubleTap
1
onDoubleTap = {}
  • 作用:处理双击事件。
  • 触发条件:当用户在组件上快速连续点击两次时触发。
  • 适用场景:常用于实现双击放大图片、双击刷新等功能。
  1. onLongPress
1
onLongPress = {}
  • 作用:处理长按事件。
  • 触发条件:当用户在组件上按住一段时间(通常为500ms以上)时触发。
  • 适用场景:常用于实现长按弹出菜单、长按拖动元素等功能。
  1. onPress
1
onPress = {}
  • 作用:处理按下事件。
  • 触发条件:当用户在组件上按下时触发,无论是否释放。
  • 适用场景:常用于实现按下时的视觉反馈、按下时开始某些操作等功能。

2.滚动修饰符

verticalScroll

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun Scroll(){
val state = rememberScrollState()
LaunchedEffect(Unit) {
state.animateScrollTo(100)
}
Column(modifier = Modifier.background(Color.LightGray).padding(2.dp).size(100.dp).verticalScroll(state)) {
repeat(10){
Text(text = "this is $it", modifier = Modifier.padding(2.dp))
}
}
}

3.可滚动修饰符

Scrollable,可以检测滑动手势,而不改变界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun ScrollAble(){
var offset by remember {
mutableStateOf(0f) //记录偏移量
}

Box(modifier = Modifier
.size(200.dp)
.scrollable(
orientation = Orientation.Vertical, //记录垂直方向的
state = rememberScrollableState { delta ->
offset += delta
delta
}
), contentAlignment = Alignment.Center){
Text(text = offset.toString())
}
}

4.嵌套滚动

嵌套滚动机制允许一个可滚动的组件在其内部包含另一个可滚动的组件,并且两者可以协调滚动行为,使得用户体验更加流畅和自然。

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
@Composable
fun CloumTest(){

val gradient = Brush.verticalGradient( //构造渐变器
0f to Color.Green,1000f to Color.Yellow
)

Box(modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
.verticalScroll(
rememberScrollState()
)
.padding(5.dp)){
Column {
repeat(6){
Text(
text = "this is $it",
modifier = Modifier
.height(120.dp).background(gradient).fillMaxWidth()
.border(12.dp, Color.DarkGray)
.padding(24.dp)
.verticalScroll(rememberScrollState())
)
}
}
}
}

构造渐变

1
2
3
val gradient = Brush.verticalGradient( //构造渐变器
0f to Color.Green,1000f to Color.Yellow
)

5.拖动

实现拖动单一方向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
fun DragWin(){
var offsetX by remember {
mutableStateOf(0f) //记录水平的偏移量
}
Text(modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.size(150.dp)
.draggable( //设置为可拖动
orientation = Orientation.Horizontal, //检测水平位置
state = rememberDraggableState { delta ->
offsetX += delta
}
),text = "dragable")
}

任意位置拖动

使用pointerInput的detectDragGestures检测拖动手势

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
//需要多个手势
@Composable
fun DragEveryWhere(){
Box(modifier = Modifier.fillMaxSize()){
var offsetX by remember {
mutableStateOf(0f)
}
var offsetY by remember {
mutableStateOf(0f)
}

Box(
modifier = Modifier
.background(Color.Blue)
.size(50.dp)
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}){
Text(text = "drag")
}
}
}

6.多点触控

平移,旋转,缩放

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 GraSample(){
var scaleoffset by remember { //缩放
mutableStateOf(1f)
}
var rotation by remember { //旋转
mutableStateOf(0f)
}

var offset by remember { //平移
mutableStateOf(Offset.Zero)
}

val state = rememberTransformableState { zoomChange, panChange, rotationChange ->
scaleoffset += zoomChange
rotation += rotationChange
offset += panChange
}

Box(modifier = Modifier.background(Color.Yellow).size(200.dp)
.graphicsLayer {
scaleX = scaleoffset
scaleY = scaleoffset
rotationZ = rotation
rotationX = offset.x
rotationY = offset.y
}
.transformable(state)) {

}
}

7.View&Compose

1.View中使用Compose

1
2
3
4
<androidx.compose.ui.platform.ComposeView
android:id="@+id/tv_main"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

在代码中定义获取实例

1
2
3
4
5
6
7
8
// 获取 ComposeView 的实例
val composeView = findViewById<ComposeView>(R.id.tv_main)

// 使用 setContent 方法设置 ComposeView 的内容
composeView.setContent {
// 在这里定义你的 Compose UI
Text("Hello, Compose!")
}

2.Compose中使用View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun AndroidDesc(desc:String){
val htmldesc = remember(desc) {
HtmlCompat.fromHtml(desc, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
AndroidView(factory = {context ->
TextView(context).apply {
//设置TextView为可点击
movementMethod = LinkMovementMethod.getInstance()
}
}, update = { textView ->
textView.text = htmldesc
})
}

3.Navigation

1.添加依赖项

确保在你的 build.gradle 文件中添加了以下依赖项:

1
2
3
4
dependencies {
val nav_version = "2.8.9"
implementation("androidx.navigation:navigation-compose:$nav_version")
}
1
implementation(libs.androidx.navigation.compose)

2. 创建 NavControllerNavHost

在你的可组合项中,创建一个 NavController 实例,并使用 NavHost 来定义导航图。

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
@Composable
fun MyApp() {
// 创建 NavController 实例
val navController = rememberNavController()

// 使用 NavHost 定义导航图
NavHost(
navController = navController,
startDestination = "home" // 设置初始目的地
) {
// 定义可组合项作为导航目的地
composable("home") {
HomeScreen(navController)
}
composable("details") {
DetailsScreen(navController)
}
}
}

// 示例:主页屏幕
@Composable
fun HomeScreen(navController: NavController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Home Screen")
Button(onClick = { navController.navigate("details") }) {
Text("Go to Details")
}
}
}

// 示例:详情屏幕
@Composable
fun DetailsScreen(navController: NavController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Details Screen")
Button(onClick = { navController.navigateUp() }) {
Text("Back to Home")
}
}
}

点击按钮切换

1
navController.navigate(name)

3.参数的传递

1
2
3
4
5
6
7
8
composable("home/{name}", arguments = listOf(
navArgument("name"){
type = NavType.StringType //指定name的类型为String
}
)){entry->
val text = entry.arguments?.getString("name") //获取参数
Text(text = "test1 $text")
}

4.深层链接

1. 定义深层链接

composable 函数中,使用 deepLinks 参数定义深层链接。deepLinks 参数接受一系列 NavDeepLink 对象,可以使用 navDeepLink 方法快速创建。

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
@Composable
fun MyApp() {
val navController = rememberNavController()

NavHost(navController = navController, startDestination = "home") {
composable(
route = "home",
deepLinks = listOf(
navDeepLink {
uriPattern = "https://www.example.com/home"
}
)
) {
HomeScreen(navController)
}

composable(
route = "details/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType }),
deepLinks = listOf(
navDeepLink {
uriPattern = "https://www.example.com/details/{userId}"
}
)
) { backStackEntry ->
DetailsScreen(
navController,
userId = backStackEntry.arguments?.getString("userId") ?: ""
)
}
}
}

2. 配置 AndroidManifest.xml

为了使应用能够处理深层链接,需要在 AndroidManifest.xml 中为活动添加 <intent-filter>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>

3. 处理深层链接

在目标可组合项中,可以通过 backStackEntry.arguments 获取传递的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun DetailsScreen(navController: NavController, userId: String) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("User ID: $userId")
Button(onClick = { navController.navigateUp() }) {
Text("Back to Home")
}
}
}

4. 测试深层链接

你可以通过以下方式测试深层链接:

  1. 使用 adb 命令

    bash复制

    1
    adb shell am start -d "https://www.example.com/details/123" -a android.intent.action.VIEW
  2. 在浏览器中打开链接: 打开浏览器并访问 https://www.example.com/details/123,应用应该会启动并导航到详情页面。

高级用法

如果你需要更复杂的深层链接处理,可以使用类型安全的深层链接。这需要定义一个数据类来表示深层链接的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Serializable
data class Profile(val userId: String)

@Composable
fun MyApp() {
val navController = rememberNavController()

NavHost(navController = navController, startDestination = "home") {
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "https://www.example.com/profile")
)
) { backStackEntry ->
ProfileScreen(userId = backStackEntry.toRoute<Profile>().userId)
}
}
}

8.附带效果

LaunchedEffect 第一次调用Compose函数时被调用

DisposableEffect 页面退出时调用

sideEffectcompose函数每次执行都会调用该方法

1.LaunchedEffect

  • LaunchedEffectkey1 参数绑定到 showDialog 状态。当 showDialog 的值发生变化时,LaunchedEffect 会重新执行。
  • LaunchedEffect 中,你可以执行一些副作用操作,比如延迟关闭对话框。
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
@Composable
fun MessagesBox() {
var showDialog by remember { mutableStateOf(true) }

LaunchedEffect(key1 = showDialog) {
if (showDialog) {
// 这里可以执行一些异步操作,比如延迟关闭对话框
delay(3000) // 假设对话框显示3秒后自动关闭
showDialog = false
}
}

Scaffold(
topBar = {
TopAppBar(title = { Text(text = "rc") })
},
content = { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Button(onClick = { showDialog = true }) {
Text(text = "Click Me")
}
}
}
)

if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Dialog Title") },
text = { Text("This is a dialog message.") },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("OK")
}
}
)
}
}

2.rememberCoroutineScope

主要作用:

  1. 生命周期绑定
    • rememberCoroutineScope 创建的 CoroutineScope 会与组合的生命周期绑定。当组合被销毁时,该作用域内的所有协程都会被自动取消,从而避免内存泄漏。
  2. 启动协程
    • 在 Compose 中,你可以使用 rememberCoroutineScope 获取的 CoroutineScope 来启动协程,执行异步或耗时操作。
  3. 状态管理
    • 通常与可变状态(如 mutableStateOf)结合使用,通过协程更新 UI 状态,触发界面重组。
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Composable
fun MyBoxed() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }

ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "test", fontSize = 24.sp)
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "rc") },
navigationIcon = {
IconButton(onClick = {
scope.launch {
drawerState.open()
}
}) {
Icon(
imageVector = Icons.Default.List,
contentDescription = "Open drawer"
)
}
}
)
},
content = { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Button(onClick = {
showDialog = true
}) {
Text(text = "Click Me")
}
}

if (showDialog) {
AlertDialog(
onDismissRequest = {
showDialog = false
},
title = { Text("Dialog Title") },
text = { Text("This is a dialog message.") },
confirmButton = {
TextButton(onClick = {
showDialog = false
}) {
Text("OK")
}
}
)
}
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = { },
icon = { Icon(Icons.Filled.Edit, "Extended FAB") },
text = { Text(text = "Extended FAB") },
)
},
)
}
}

3.rememberUpdatedState

保证被使用时是最新值,参数被改变时不重启effect

  1. 捕获最新状态
    • 在组合过程中,状态值可能会发生变化。rememberUpdatedState 会捕获最新的状态值,并在协程或其他异步操作中使用这个最新值。
  2. 避免状态不一致
    • 如果直接在协程中使用组合时的状态值,可能会导致状态不一致的问题。rememberUpdatedState 确保协程中使用的是最新的状态值。
  3. 简化状态管理
    • 它提供了一种简单的方式在协程中管理状态的更新,而不需要手动处理状态的捕获和更新。
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
@Composable
fun RememberUpdatedStateExample() {
var count by remember { mutableStateOf(0) }
val currentCount = rememberUpdatedState(count)

val coroutineScope = rememberCoroutineScope()

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Count: $count", fontSize = 24.sp)
Button(
onClick = {
coroutineScope.launch {
delay(1000) // 模拟异步操作
println("Current count in coroutine: ${currentCount.value}")
}
count++ // 更新状态
}
) {
Text("Increment and Launch Coroutine")
}
}
}

9.项目管理

占位符

为了更优雅的完成上述的问题,我们可以在strings.xml文件里,使用%s %1$s %d %1$d这些占位符,在页面设置展示的时候我们只需要将真实展示的文字或者数据替换掉就OK了。

占位符 类型
%s 替换String类型
%1$s 替换String类型
%d 替换int类型
%1$d 替换int类型

注:%s 、%d为缩写方式,只替换一个位置,可以这么写

单一占位符使用

在strings.xml定义字符串
<string name="str">截止目前你已经消费了人民币%s元</string>

多个占位符使用

在strings.xml中定义
<string name="str">大家好,我叫%1$s,我的爱好是%2$s</string>

在类中使用

String.format(mActivity.getResources().getString(R.string. str), "Rc","code"))

最终的输出结果:大家好,我叫Rc,我的爱好是code

在Compose中使用

在string.xml文件中定义
<string name="item">this is a %1$s and %2$d</string>

1
Text(text = String.format(stringResource(id = R.string.item),"test",30))

显示到状态栏

//允许显示到状态栏内,文字显示为白色 enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))