好处
Compose 编译后不是转化为原生的 Android 上的 View 去显示,而是依赖于平台的 Canvas
,在这点上和 Flutter 有点相似,简单地说可以理解为 Compose 是全新的一套 View 。
声明式 UI,通过对比可以看到 Kotin DSL 有诸多好处:
- 有着近似 XML 的结构化表现力
- 较少的字符串,更多的强类型,更安全
- 可提取 linearLayoutParams 这样的对象方便复用
- 在布局中同步嵌入 onClick 等事件处理
- 如需要还可以嵌入 if ,for 这样的控制语句
- 减少 findViewById 等函数遍历树
- 加速开发:View 与 Compose 之间可以相互调用,兼容现有的所有代码。借助 AS 可以实时预览界面,轻松执行界面检查。
- 另外 Compose 里的代码基本都是可以被混淆的,所以开启混淆之后代码的压缩率也很高。
- 手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。
入门
Jetpack Compose 中的 match_parent 相当于什么?
Compose 编程思想 | Jetpack Compose | Android Developers
Compose 布局基础知识 | Jetpack Compose | Android Developers
原创:写给初学者的Jetpack Compose教程,基础控件和布局
原创:写给初学者的Jetpack Compose教程,Modifier
原创:写给初学者的Jetpack Compose教程,使用State让界面动起来
原创:写给初学者的Jetpack Compose教程,Lazy Layout
[[android编译优化#AGP、kotlin、compose对应版本]]
Composable
告诉编译器:此函数旨在将数据转换为界面。
所有的 Composable 函数还有一个约定俗成的习惯,就是函数的命名首字母需要大写。
@Preview 注解,这个注解表示这个函数是用来快速预览 UI 样式的。
@Composable 注解用于标记一个函数为可组合函数。可组合函数是一种特殊的函数,不需要返回任何 UI 元素,因为可组合函数描述的是所需的屏幕状态,而不是构造界面 widget;而如果按我们以前的 XML 编程方式,必须在方法中返回 UI 元素才能使用它(如返回 View 类型)。
@Composable 注解的函数之间可以相互调用,因为这样 Compose 框架才能正确处理依赖关系。另外,@Composable 函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable 函数。 这里可以类比下 kotlin 中 suspend 挂起函数的用法,其用法是相似的。
布局
Compose 通过只测量一次子项来实现高性能。单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。
父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整。
其他组件
CollapsingToolbarScaffold
stickyHeader
HorizontalPager
BottomNavigationBar
Scaffold
PullRefreshIndicator
TopAppBar
列表
列表和网格 | Jetpack Compose | Android Developers
我们可以使用 verticalScroll()
修饰符使 Column
可滚动
1 2 3 4 5 6 7 8
| @Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
|
延迟列表
使用 Compose 的 LazyColumn
和 LazyRow
。这些可组合项只会呈现屏幕上显示的元素,因此,对于较长的列表,使用它们会非常高效。
1 2 3 4 5 6 7 8 9 10
| import androidx.compose.foundation.lazy.items
@Composable fun MessageList(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageRow(message) } } }
|
还有一个名为 [itemsIndexed ()
]( https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/package-summary?hl=zh-cn# (androidx. compose. foundation. lazy. LazyListScope). itemsIndexed (kotlin. collections. List, kotlin. Function2, kotlin. Function2, kotlin. Function3)) 的 items ()
扩展函数的变体,用于提供索引
内容内边距
1 2 3 4 5
| LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
|
如需在列表项之间添加间距,可以使用 Arrangement.spacedBy ()
。以下示例在每个列表项之间添加了 4.dp
的间距:
1
| verticalArrangement = Arrangement.spacedBy(4.dp),
|
性能
早期 Lazy Layout 的性能很差,滚动的时候巨卡无比,确实很难让人用得下去。
但是在 Compose 1.5版本中,Google 做了大量的性能优化工作,所以如果你现在再来尝试一次,你会发现性能已经不是什么问题了。
修饰符
借助修饰符,您可以修饰或扩充可组合项。您可以使用修饰符来执行以下操作:
- 更改可组合项的大小、布局、行为和外观
- 添加信息,如无障碍标签
- 处理用户输入
- 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放
修饰符是标准的 Kotlin 对象。您可以通过调用某个 Modifier
类函数来创建修饰符:
1 2 3 4 5 6 7 8 9 10 11 12
| @Composable fun ArtistCard() { val padding = 16.dp Column( Modifier .clickable(onClick = onClick) .padding(padding) .fillMaxWidth() ) { } }
|
- 修饰符顺序很重要
- 提取和重复使用修饰符
clickable
使可组合项响应用户输入,并显示涟漪。
padding
在元素周围留出空间。
fillMaxWidth
使可组合项填充其父项为它提供的最大宽度。
size()
指定元素的首选宽度和高度。
偏移量
要相对于原始位置放置布局,请添加 offset
修饰符,并在 x 轴和 y 轴中设置偏移量。偏移量可以是正数,也可以是非正数。padding
和 offset
之间的区别在于,向可组合项添加 offset
不会改变其测量结果:
1 2 3 4 5 6 7 8 9 10 11 12
| @Composable fun ArtistCard(artist: Artist) { Row() { Column { Text(artist.name) Text( text = artist.lastSeenOnline, modifier = Modifier.offset(x = 4.dp) ) } } }
|
offset
修饰符根据布局方向水平应用。在从左到右的上下文中,正 offset
会将元素向右移,而在从右到左的上下文中,它会将元素向左移。
requiredSize
请注意,如果指定的尺寸不符合来自布局父项的约束条件,则可能不会采用该尺寸。如果您希望可组合项的尺寸固定不变,而不考虑传入的约束条件,请使用 requiredSize
修饰符:
1 2 3 4 5 6 7 8 9 10 11 12
| @Composable fun ArtistCard() { Row( modifier = Modifier.size(width = 400.dp, height = 100.dp) ) { Image( modifier = Modifier.requiredSize(150.dp) ) Column { } } }
|
在此示例中,即使父项的 height
设置为 100.dp
,Image
的高度还是 150.dp
,因为 requiredSize
修饰符优先级较高。
滚动
在 View 中的话,通常可以在需要滚动的内容之外再嵌套一层 ScrollView 布局,这样 ScrollView 中的内容就可以滚动了。
而 Compose 则不需要再进行额外的布局嵌套,只需要借助 modifier 参数即可,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11
| @Composable fun SimpleWidgetColumn() { Row( modifier = Modifier .fillMaxSize() .horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically, ) { ... } }
|
添加间距 Spacer
1
| Spacer(modifier = Modifier.width(8.dp))
|
如何才能给 Button 指定文字内容呢?它可以和 Text 配合在一起使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Button( onClick = { }, contentPadding = PaddingValues( start = 20.dp, top = 12.dp, end = 20.dp, bottom = 12.dp ) ) { Icon( Icons.Filled.Favorite, contentDescription = "Favorite", modifier = Modifier.size(ButtonDefaults.IconSize) ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Text("Like") }
|
Context
要想弹出 Toast 需要有 Context 参数才行。在 Composable 函数当中获取 Context 对象,可以调用 LocalContext. current 获得。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Composable fun SimpleWidgetColumn() { Column { ... val context = LocalContext.current Button(onClick = { Toast.makeText(context, "This is Toast", Toast.LENGTH_SHORT).show() }) { Text( text = "This is Button", color = Color.White, fontSize = 26.sp ) } } }
|
文字图片
Compose 中的文字 | Jetpack Compose | Android Developers
自定义图片 | Jetpack Compose | Android Developers
1 2 3 4 5 6 7 8 9 10
| val imageModifier = Modifier .size(150.dp) .border(BorderStroke(1.dp, Color.Black)) .background(Color.Yellow) Image( painter = painterResource(id = R.drawable.dog), contentDescription = stringResource(id = R.string.dog_content_description), contentScale = ContentScale.Fit, modifier = imageModifier )
|
TextField
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Composable fun SimpleWidgetColumn() { Column { ... TextField( value = "", onValueChange = {}, placeholder = { Text(text = "Type something here") }, colors = TextFieldDefaults.textFieldColors( backgroundColor = Color.White ) ) } }
|
重组
- 为了跟踪这种状态变化,您必须使用
remember
和 mutableStateOf
函数。
- remember 和 mutableStateOf 在 Composable 函数中几乎永远都是配套使用的。
- 使用 by 关键字替代了之前的等号,用委托的方式来为 count 变量赋值。count 的类型是 MutableState<Int>,而改用 by 关键字赋值之后,count 的类型就变成了 Int。既然都是 Int 了,那么我们就可以直接对这个值进行读写操作了,而不用像之前那样再调用它的 getValue ()和 setValue ()函数,是不是代码变得更简单了?注意导包
1 2
| import androidx. compose. runtime. getValue import androidx. compose. runtime. setValue
|
- rememberSaveable 函数是 remember 函数的一个1增强版,它唯一和 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import androidx. compose. foundation. clickable import androidx. compose. runtime. getValue import androidx. compose. runtime. mutableStateOf import androidx. compose. runtime. remember import androidx. compose. runtime. setValue
class MainActivity : ComponentActivity () { override fun onCreate (savedInstanceState: Bundle?) { super.onCreate (savedInstanceState) setContent { ComposeTutorialTheme { Conversation (SampleData. conversationSample) } } } }
@Composable fun MessageCard (msg: Message) { Row (modifier = Modifier.padding (all = 8. dp)) { Image ( painter = painterResource (R.drawable. profile_picture), contentDescription = null, modifier = Modifier .size (40. dp) .clip (CircleShape) .border (1.5. dp, MaterialTheme. colors. secondaryVariant, CircleShape) ) Spacer (modifier = Modifier.width (8. dp)) var isExpanded by remember { mutableStateOf (false) }
Column (modifier = Modifier. clickable { isExpanded = !isExpanded }) { Text ( text = msg. author, color = MaterialTheme. colors. secondaryVariant, style = MaterialTheme. typography. subtitle2 )
Spacer (modifier = Modifier.height (4. dp))
Surface ( shape = MaterialTheme. shapes. medium, elevation = 1. dp, ) { Text ( text = msg. body, modifier = Modifier.padding (all = 4. dp), maxLines = if (isExpanded) Int. MAX_VALUE else 1, style = MaterialTheme. typography. body2 ) } } } }
|
状态提升
以下是你应该考虑的状态提升最少应该到达哪个层级的关键因素:
如果有多个 Composable 函数需要读取同一个 State 对象,那么至少要将 State 提升到这些 Composable 函数共有的父级函数当中。
如果有多个 Composable 函数需要对同一个 State 对象进行写入,那么至少要将 State 提升到所有执行写入的 Composable 函数里调用层级最高的那一层。
如果某个事件的触发会导致两个或更多的 State 发生变更,那么这些 State 都应该提升到相同的层级。
viewmodel
首先我们要引入如下两个库,这是 Compose 为了适配 ViewModel 和 LiveData 而专门设计的库:
1 2 3 4
| dependencies { implementation "androidx. lifecycle: lifecycle-viewmodel-compose: 2.6.2" implementation "androidx. compose. runtime: runtime-livedata: 1.5.1" }
|
传统 LiveData 的用法在 Compose 中并不好使,因为传统 LiveData 依赖于监听某个值的变化,并对相应的界面进行更新,而 Compose 的界面更新则依赖于重组。
因此,我们需要将 LiveData 转换成 State 才行,observeAsState ()函数就是用来做这个事情的,参数中传入的0表示它的初始值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import androidx. lifecycle. viewmodel. compose. viewModel
@Composable fun CallCounter (modifier: Modifier = Modifier, viewModel: MainViewModel = viewModel ()) { val count by viewModel.count.observeAsState (0) val doubleCount by viewModel.doubleCount.observeAsState (0) Column { Counter ( count = count, onIncrement = { viewModel.incrementCount () }, modifier.fillMaxWidth () ) Counter ( count = doubleCount, onIncrement = { viewModel.incrementDoubleCount () }, modifier.fillMaxWidth () ) } }
|
互相调用
Interoperability API | Jetpack Compose | Android Developers
Jetpack Compose和View的互操作性 - 圣骑士wind - 博客园
ComposeView
的 setContent (content: @Composable () -> Unit)
方法只有一个 content
参数,而这个参数是一个添加了 @Composable
注解的匿名函数,也就是说,在其中我们可以正常的使用 compose 了。
1 2 3
| bind. jointGifPreviewRecyclerView. setContent { Test () }
|
Android View
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 CustomView () { val state = remember { mutableStateOf (0) } //widget. Button AndroidView ( factory = { ctx -> //Here you can construct your View android.widget.Button (ctx). apply { text = "My Button" layoutParams = LinearLayout.LayoutParams (MATCH_PARENT, WRAP_CONTENT) setOnClickListener { state. value++ } } }, modifier = Modifier.padding (8. dp) ) //widget. TextView AndroidView (factory = { ctx -> //Here you can construct your View TextView (ctx). apply { layoutParams = LinearLayout.LayoutParams (MATCH_PARENT, WRAP_CONTENT) } }, update = { it. text = "You have clicked the buttons: " + state.value.toString () + " times" }) }
|
这里的桥梁是AndroidView
, 它是一个 composable 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Composable fun <T : View> AndroidView ( factory: (Context) -> T, modifier: Modifier = Modifier, update: (T) -> Unit = NoOpUpdate ) ````
Compose 和 View 的结合, 主要是靠两个桥梁. 还挺有趣的:
- `ComposeView`其实是个 Android View. - `AndroidView`其实是个 Composable 方法.
Compose 和 View 可以互相兼容的特点保证了项目可以逐步迁移, 并且也给够了安全感, 像极了当年 java 项目迁移 kotlin,至于什么学习曲线, 经验不足, 反正早晚都要学的, 整点新鲜的也挺好
## 项目学习
[fmtjava/Compose_Eyepetizer: 一款基于 Jetpack Compose 实现的精美仿开眼视频App(提供Kotlin、Flutter、React Native、小程序版本 😁 )](https:
### 其他
|
val pagerState = rememberPagerState ()
1 2 3
| HorizontalPager 是一种用于构建横向滚动页面的组件。它允许您在应用程序中创建水平滑动的页面布局,类似于 ViewPager 或 RecyclerView。
|
HorizontalPager (pageCount = 4,
userScrollEnabled = false,
state = pagerState,
modifier = Modifier
.fillMaxSize ()
.background (Color. White)
.padding (padding)) { pageIndex ->
when (pageIndex) {
0 -> DailyPage ()
1 -> DiscoverPage ()
2 -> HotPage ()
3 -> PersonPage ()
}
}
1 2 3
| 使用记忆的协程作用域可以确保在组件重新合成时,作用域内的协程会自动取消
|
val scope = rememberCoroutineScope ()
1 2
| weight 用于设置子元素的权重,权重越大,占据的空间就越大
|
Column (modifier = Modifier.weight (1f)
Divider (thickness = 0.5. dp, modifier = Modifier.padding (top = 5. dp))
Box (modifier = Modifier
.padding (2. dp)
.clip (CircleShape)
.background (color)
.size (8. dp))
Box (modifier = Modifier
.padding (end = 15. dp, bottom = 10. dp)
.background (Black_54, shape = RoundedCornerShape (5. dp))
.padding (5. dp)
.align (Alignment. BottomEnd), contentAlignment = Alignment. Center) {
Text (text = DateUtils.formatDateMsByMS ((itemData. duration * 1000). toLong ()),
color = Color. White,
fontWeight = FontWeight. Bold)
Text (text = DateUtils.formatDateMsByMS ((itemData. duration * 1000). toLong ()),
color = Color. White,
fontWeight = FontWeight. Bold,
modifier = Modifier
.padding (end = 15. dp, bottom = 10. dp)
.background (Black_54, shape = RoundedCornerShape (5. dp))
.padding (5. dp)
.align (Alignment. BottomEnd))
1 2 3 4 5 6 7 8 9
| 注意:*这里有2个 padding*
![image.png](https://obsidian-yupic.oss-cn-beijing.aliyuncs.com/picgo/picgo-clipboard-images/2024-01-11-17-24-9f63093e4674da562ad47dc95ea31c55-20240111172445-8d24e6.png)
### padding
在 Compose 中,确实没有`margin`修饰符,只有`padding`修饰符。如果您想在`Text`组件周围创建间距,可以使用`padding`修饰符来实现类似的效果。在您提供的示例代码中,`Modifier.padding (top = 3. dp)`将在`Text`组件的顶部添加3dp 的内边距,从而创建了与`margin`类似的效果。
|
Text (text = itemData. author?. description ?: “”,
color = Color. White,
fontSize = 13. sp, modifier = Modifier.padding (top = 3. dp))
Spacer (modifier = Modifier.height (10. dp))
1 2 3 4 5 6 7 8 9
|
### zIndex
`zIndex` 是指定视图的层级顺序的属性。它控制了视图在屏幕上的显示顺序。具有较高 `zIndex` 值的视图将显示在具有较低 `zIndex` 值的视图之上。
默认情况下,视图的 `zIndex` 值为0。如果设置一个较大的正值,则视图将显示在其他视图的上方。如果设置一个较小的负值,则视图将显示在其他视图的下方。当两个视图的 `zIndex` 相同时,它们将按照它们在布局文件中的顺序进行绘制。
通过调整视图的 `zIndex` 属性,您可以控制视图的叠加顺序,从而达到覆盖或隐藏其他视图的效果。
|
DiscoverTabPageWidget (pagerState, modifier = Modifier
.weight (1f)
.zIndex (-1f))
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ### LaunchedEffect
`LaunchedEffect` 是 Jetpack Compose 中的一个函数,用于在协程中执行副作用操作。副作用操作通常包括异步任务、网络请求、数据库操作或其他可能会阻塞主线程的操作。
`LaunchedEffect` 函数是一个协程构建器,它接受一个或多个参数,并在代码块中执行异步操作。它会自动在适当的时间启动和取消协程,确保在 Compose 组件的生命周期内正确处理副作用。当组件被创建时,`LaunchedEffect` 会启动协程,当组件被销毁时,它会自动取消协程。
`LaunchedEffect` 的参数可以是任何对象,用于标识不同的副作用操作。通常使用简单的数据类对象作为参数,例如 `key1 = Unit`。
在 `LaunchedEffect` 的代码块中,你可以执行各种需要在后台进行的操作,例如网络请求、数据库访问、文件读写等。由于这些操作是在协程中执行的,因此它们不会阻塞主线程,确保应用保持响应性。
需要注意的是,`LaunchedEffect` 函数只能在 Compose 函数内部调用,例如在 `@Composable` 注解的函数内部使用。如果你尝试在非 Compose 函数中调用它,将会出现编译错误。
总结起来,`LaunchedEffect` 是一个用于在协程中执行副作用操作的函数,它确保在 Compose 组件的生命周期内正确处理副作用。它是 Jetpack Compose 中处理异步任务和副作用的重要工具之一
|
itemData?. run {
LaunchedEffect (key1 = Unit) {
viewModel.getRelateVideoList (itemData. id)
viewModel.saveVideo (itemData)
}
}
1 2 3 4 5
|
### DisposableEffect
用于在组件创建和销毁时执行一些副作用操作。当组件被销毁时,onDispose 代码块内的操作会被执行
|
val coroutineScope = rememberCoroutineScope ()
DisposableEffect (Unit) {
val timer = Timer ()
timer.schedule (object : TimerTask () {
override fun run () {
coroutineScope. launch {
pagerState.animateScrollToPage (pagerState. currentPage + 1)
}
}
}, 3000, 3000)
onDispose {
timer.cancel ()
}
}