1-开发实战

fmtjava/Compose_Eyepetizer: 一款基于 Jetpack Compose 实现的精美仿开眼视频App(提供Kotlin、Flutter、React Native、小程序版本 😁 )]( https://github.com/fmtjava/Compose_Eyepetizer)

navController

1
2
3
4
5
6
7
navController.navigate(screen.route) {  
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}

popUpTo (navController. graph. startDestinationId)
作用:导航时清除回退栈到指定目的地
效果:清除到起始目的地之间的所有页面,避免回退栈无限增长
saveState = true
作用:保存被清除页面的状态
效果:当用户返回时,页面状态得以保留
launchSingleTop = true
作用:如果目标页面已在栈顶,则不创建新实例
效果:避免重复创建相同页面
restoreState = true
作用:恢复目标页面之前保存的状态
效果:提升用户体验,保留页面之前的交互状态

val navController = rememberNavController()
作用:创建并记住一个 NavHostController 实例,用于管理应用内的导航
原理:rememberNavController () 是一个可组合函数,它确保在重组时返回相同的 NavHostController 实例
用途:通过这个控制器可以执行导航操作,如 navigate ()、popBackStack () 等

变量写法

StateFlow

  1. 状态封装原则
    使用 private val _smsState 作为内部可变状态

使用 val smsState 作为对外暴露的不可变状态流

遵循了状态封装的最佳实践,防止外部直接修改状态

  1. StateFlow 特性
    MutableStateFlow<SmsState> 创建了一个可变的状态流
    asStateFlow() 将可变流转换为只读流暴露给外部

  2. 状态管理优势
    单向数据流:外部只能观察状态,不能直接修改
    线程安全:协程 Flow 保证了状态更新的线程安全性
    生命周期感知:配合 Compose 或 Lifecycle 可以自动处理订阅生命周期

  3. 使用场景
    在 SmsCodePage 中可以通过.collect或.collectAsState来观察 smsState 的变化,根据不同的状态(Loading、Success、Error、Idle)来更新 UI 显示。

1
2
3
private val _smsState = MutableStateFlow<SmsState>(SmsState. Idle)
val smsState: StateFlow<SmsState> = _smsState.asStateFlow ()
val state by viewModel.smsState.collectAsState ()

自动重组:当 Flow 发射新值时,Compose 会触发 UI 重组。

生命周期感知:在 @Composable 环境中使用时,会自动跟随 Lifecycle 取消订阅,避免内存泄漏。

对比项 collect () collectAsState ()
使用环境 任意协程环境 仅 Compose
返回类型 无返回值 (suspend 函数) State (Compose 可观察)
是否触发重组 否❌
是否自动处理生命周期 否,需要手动管理✅ Compose 自动管理
常见场景 ViewModel、Repository、后台逻辑 Compose UI 层

collectAsState

Jetpack Compose 中常用的一个扩展函数,用来把 Flow/StateFlow 转换成 Compose 可感知的 State<T>,从而让 UI 自动响应数据变化。
自动重组:当 Flow 发射新值时,Compose 会触发 UI 重组。
生命周期感知:在 @Composable 环境中使用时,会自动跟随 Lifecycle 取消订阅,避免内存泄漏

mutableStateOf (不推荐)

1
2
// 👇 新增:用 mutableStateOf 来保存 currentStep,这样 Compose 能自动重组
var currentStep by mutableIntStateOf(1)
1
2
3
4
LaunchedEffect(nextStep) {
viewModel.currentStep = nextStep
}
val currentStep = viewModel.currentStep

问题

  • viewModel 的currentStep 变化 page 的currentStep 也会变化吗,没看到LaunchedEffect(currentStep) { 也会重组吗

会的——只要 currentStep 在 ViewModel 里是 mutableStateOf(或 StateFlow 转成 State),页面里读取到了这个 state,它变更时就会触发重组。不需要 LaunchedEffect(currentStep) 来驱动 UI 更新。

  • 为什么不需要 LaunchedEffect

Compose 会自动跟踪在组合阶段被读取的 State(如 mutableStateOf)。
当这个 State 的值变化时,读取它的 Composable 会自动 recompose。
LaunchedEffect 用于副作用(如发起请求、导航、动画),不是用来让 UI 监听 state 的。
若你用的是 StateFlow,同理,在 VM 暴露 StateFlow<Int>,在 UI 用 collectAsState() 即可

  • val accountInfoBean by viewModel. accountInfoBean
    会的。只要 viewModel. accountInfoBean 是一个 mutableStateOf(或 State<T>),那这行代码就已经在 Compose 中建立了订阅关系。当 ViewModel 中的值变化时,UI 会自动重组。

小结

✅ 读 mutableStateOf/collectAsState() → 值变化自动重组
❌ 不需要 LaunchedEffect(currentStep)
✅ 仅在需要“做一次事”(如用 nextStep 初始化、发请求)时用 LaunchedEffect(…)

liveData

(1)State<T> 是 Compose 的“原生”状态类型

  • Compose 的 @Composable 函数天然监听 State<T> 的变化并触发重组。
  • LiveData 需要通过 observeAsState() 转换为 State 才能在 Compose 中使用,多了一层转换。
  • 直接使用 mutableStateOf 更简洁、高效,无需依赖 Android 生命周期组件。

(2)LiveData 是为传统 View 系统设计的

  • LiveData 的核心优势是 生命周期感知(自动在 onStop 时停止通知),防止内存泄漏。
  • 但在 Compose 中,UI 的生命周期由 Compose 自己管理(通过 DisposableEffectLaunchedEffect 等),不再依赖 LifecycleOwner
  • 因此 LiveData 的生命周期感知优势在 Compose 中 变得冗余甚至多余。

(3)StateFlow / SharedFlow 更适合现代 Compose 架构

修改传入的值、rememberSaveable

1
2
3
4
5
6
7
8
@Composable
fun UpdateInfoPage(nextStep: Int) {
val viewModel: UpdateInfoViewModel = hiltViewModel()
val context = LocalContext.current
val navController = LocalNavController.current

// 这里使用 rememberSaveable 保持步骤状态可修改、可记忆
var currentStep by rememberSaveable { mutableIntStateOf(nextStep) }

❗不要用 remember 包住;保证每次点击都拿到“最新的权限/隐私状态”

智能重组的坑

Compose 的重组是 “按读取字段” 触发的,不是“按对象引用”。

举个例子

1
2
3
4
5
6
7
@Composable
fun HomePageContent(orderState: OrderStateBean) {
Text(text = orderState.title) // 只读了 title
if (orderState.hasUnread) { // 只读了 hasUnread
Badge()
}
}

🔥 即使 orderStateBean.value = newBean(整个对象变了),只要 titlehasUnread 的值没变,HomePageContent 就不会重组!

生命周期

1
2
3
4
LifecycleResumeEffect(Unit) {
launchInProgress = false
onPauseOrDispose { }
}

好图片

00bb3c10cefd1a117f526af3ecb4f0c0_MD5

标准代码结构

UI 与逻辑分离 + 只对纯状态组件预览

  • 将 UI 拆分为两个函数:

    1. SmsCodePage:负责和 ViewModel、NavController、状态管理交互(不预览)
    2. SmsCodeContent:只接收 UI 状态(如 phone、otpValue、loading、error 等),负责渲染(可预览)
  • SmsCodeContent 添加 @Preview,让它能独立展示各种 UI 状态。

api用法

any

true:如果集合中 至少有一个 元素满足条件

false:如果 没有任何 元素满足条件

val showDisburseFailedDialog = remember(ordersList) {

mutableStateOf(ordersList.any { it.checkStatus == 6 })

}

//tood 如果ordersList里 有OrderListItem 的 checkStatus 为 6是 显示showDisburseFailedDialog

takeIf

ocr. idPhotoId.takeIf { it != null } ?: oldIdentity?. idPhotoId

满足是前面,不满足是后面

只加载一次

rememberSaveable

1
2
3
4
5
6
7
8
var hasInitLoaded by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(hasInitLoaded) {
if (!hasInitLoaded) {
changeCardViewModel.getAccountInfoAndDictConfig()
refreshOrders()
hasInitLoaded = true
}
}

图标

尺寸

mdpi 48x48

hdpi 72x72

xhdpi 96x96

xxhdpi 144x144

xxxhdpi 192x192

COMPOSE不支持.9图

去掉点击效果

1
2
3
4
5
6
7
8
9
Row(
modifier = modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { onClick() },
verticalAlignment = Alignment.CenterVertically
) {

本地图片做背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
fun UpdateInfoCardContainer(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
Column(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp)) // 圆角
.paint(
painter = painterResource(id = R.drawable.profile_tab_card_bg), // 本地切图
contentScale = ContentScale.Crop // 拉伸方式
)
.padding(16.dp),
content = content
)
}

  • Data class改成class 可以混淆,要不然 toString 就又能看到混淆前的数据

函数要加()

IconButton( onClick = { onBackClick()

//onBackClick 调不了 },

先后顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Box(Modifier.navigationBarsPadding()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp) // 外边距
.height(44.dp)
.clip( RoundedCornerShape(10.dp)) // 先裁圆角
.background(Color(0xFFCCCCCC)) // 再画背景,这样会被圆角裁剪
.clickable { onConfirm() },
contentAlignment = Alignment.Center
) {
Text(
text = confirmText,
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
}

常用组件

知识点

weight 用于设置子元素的权重,权重越大,占据的空间就越大

1
Column (modifier = Modifier.weight (1f) 

thickness 分割线的厚度

1
Divider (thickness = 0.5. dp, modifier = Modifier.padding (top = 5. dp))

这是一个小圆点

1
2
3
4
5
Box (modifier = Modifier  
.padding (2. dp)
.clip (CircleShape)
.background (color)
.size (8. dp))

背景本地切图

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
private fun TimeBlock(text: String) {
Box(
modifier = Modifier
.width(120.dp)
.height(124.dp)
.clip(RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.home_countdown_time_bg),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier.matchParentSize()
)

Text(
text = text,
color = FFFDD17B,
fontSize = 48.sp,
fontWeight = FontWeight.Bold
)
}
}

渐变

1
2
3
4
5
6
Brush.horizontalGradient(
colors = listOf(
FFE46C15,
FFF8B42B,
)
)

Button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Button(
onClick = {
onLeftClick()
},
shape = shape,
colors = ButtonDefaults.buttonColors(
containerColor = FF1A1A1A // 设置背景色
),
modifier = Modifier
.weight(1f)
.border(1.dp, FF4D4C4A, shape).height(40.dp), //坑:height再最后,要不然会覆盖
) {
Text(
leftString,
fontSize = 14.sp,
color = Color.White
)
}

Text

1
2
3
4
5
6
7
8
9
10
11
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
content,
color = Color.White.copy(alpha = 0.8f),
fontSize = 14.sp,
textAlign = TextAlign.Center
)
)

lineHeight = 12.sp, 行高

Image

1
2
3
4
5
6
7
8
Image(
painter = painterResource(R.drawable.home_update_fail),
contentDescription = null,
modifier = Modifier
.align(Alignment.TopCenter)
.offset(y = -40.dp)
.requiredSize(350.dp) //突破父view的限制
)

ViewPager、HorizontalPager

//ViewPager2, 通过将此状态对象保存在组件中,可以确保当组件重新合成时,分页状态不会丢失。

1
val pagerState = rememberPagerState ()

HorizontalPager 是一种用于构建横向滚动页面的组件。它允许您在应用程序中创建水平滑动的页面布局,类似于 ViewPager 或 RecyclerView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 ()
}
}

padding

在 Compose 中,确实没有 margin 修饰符,只有 padding 修饰符。如果您想在 Text 组件周围创建间距,可以使用 padding 修饰符来实现类似的效果。在您提供的示例代码中,Modifier.padding (top = 3. dp) 将在 Text 组件的顶部添加3dp 的内边距,从而创建了与 margin 类似的效果。

1
2
3
Text (text = itemData. author?. description ?: "",  
color = Color. White,
fontSize = 13. sp, modifier = Modifier.padding (top = 3. dp))

或者:

1
Spacer (modifier = Modifier.height (10. dp))

zIndex

zIndex 是指定视图的层级顺序的属性。它控制了视图在屏幕上的显示顺序。具有较高 zIndex 值的视图将显示在具有较低 zIndex 值的视图之上。

默认情况下,视图的 zIndex 值为0。如果设置一个较大的正值,则视图将显示在其他视图的上方。如果设置一个较小的负值,则视图将显示在其他视图的下方。当两个视图的 zIndex 相同时,它们将按照它们在布局文件中的顺序进行绘制。

通过调整视图的 zIndex 属性,您可以控制视图的叠加顺序,从而达到覆盖或隐藏其他视图的效果。

1
2
3
DiscoverTabPageWidget (pagerState, modifier = Modifier  
.weight (1f)
.zIndex (-1f))

evnetBus

  • 定义一个 在 Activity 范围内共享的 ViewModel,存放你要广播的事件。
  • 各个页面通过 viewModel() / hiltViewModel() 获取同一个实例,直接订阅状态。
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
// 共享的事件中心
@HiltViewModel
class EventBusViewModel @Inject constructor() : ViewModel() {
private val _event = MutableSharedFlow<String>() // 可以换成 StateFlow
val event = _event.asSharedFlow()

suspend fun sendEvent(msg: String) {
_event.emit(msg)
}
}

//发送
val eventBus: EventBusViewModel = hiltViewModel()
LaunchedEffect(Unit) {
eventBus.sendEvent("来自页面A的消息")
}

//监听
val eventBus: EventBusViewModel = hiltViewModel()
LaunchedEffect(Unit) {
eventBus.event.collect { msg ->
Log.d("EventBus", "收到消息: $msg")
}
}

通常不需要你手动取消监听

原因有两个:

  1. collect 的生命周期是绑定在 Composable 的 LaunchedEffectDisposableEffect 上的

    • 当 Composable 离开界面时,它的协程会自动取消,Flow 的收集也会结束。
    • 所以不会像传统 EventBus 一样出现「忘记 unregister 导致泄漏」的问题。
  2. ViewModel 的作用域受 Activity/Navigation 控制

    • Activity 销毁时,ViewModel 也会销毁,内部的 Flow 也不再推送事件。

1-开发实战
http://peiniwan.github.io/2025/12/7092fe14667b.html
作者
六月的雨
发布于
2025年12月16日
许可协议