Android-Compose初步学习总结

一、Jetpack Compose的生命周期

        Jetpack Compose 中,组合是由可组合项构成的树状结构,用于描述应用界面。

        它通过初始组合生成,并仅能通过重新组合更新 —— 当应用状态变化时,Compose 会安排重新组合,仅重新执行受状态变化影响的可组合项,而非整个 UI 树。可组合项的生命周期包含三个阶段:进入组合、经历 0 次或多次重新组合、最终离开组合

        组合中可组合项的实例由其调用点(即调用可组合项的源代码位置)唯一标识。编译器会将每个调用点视为不同的位置,从多个调用点调用同一可组合项会在组合中创建多个实例,且每个实例拥有独立的生命周期。在重新组合时,若可组合项的调用点未变且输入参数未发生改变,Compose 会跳过对它的重新执行。

        例如,在条件语句中调用的可组合项,即使条件变化导致其他可组合项被添加或移除,只要自身调用点和输入未变,就会被保留且不重新组合。

@Composable
fun GreetingScreen(showWelcome: Boolean) {
    Column {
        // 固定调用点:每次组合都会执行,实例唯一
        Text("App Title")
        
        // 条件调用:showWelcome为true时进入组合
        if (showWelcome) {
            WelcomeMessage() // 调用点1:仅在条件满足时存在
        }
        
        // 固定调用点:无论条件如何,实例始终保留
        UserInputField() // 调用点2:始终存在
    }
}

@Composable
fun WelcomeMessage() {
    Text("Welcome!")
}

@Composable
fun UserInputField() {
    var text by remember { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it })
}

        当从同一调用点多次调用可组合项时,仅靠调用点可能无法唯一区分实例,此时执行顺序会影响实例识别,但这可能导致列表项排序、添加等操作时出现不必要的重新组合。

        为解决这一问题,可使用 key 可组合项,传入一个或多个值作为标识,使 Compose 能基于这些值识别组合中的实例。key 的值只需在当前调用点的可组合项调用中唯一即可,

        例如为列表中的电影项传入唯一 ID 作为 key,这样即使列表项排序或增删,Compose 也能通过 key 识别并重用原有实例,避免副作用(如网络请求)被频繁中断和重启。

未使用Key的问题

@Composable
fun MovieListWithoutKey(movies: List) {
    Column {
        movies.forEach { movie ->
            MovieItem(movie) // 仅靠执行顺序识别,排序/增删会导致大量重组
        }
    }
}

@Composable
fun MovieItem(movie: Movie) {
    LaunchedEffect(Unit) {
        // 模拟加载电影封面,若重组会被中断并重启
        loadMovieCover(movie.id)
    }
    Text(text = movie.title)
}

        当列表排序或在头部添加新电影时,所有MovieItem的执行顺序改变,Compose 会视为新实例,导致LaunchedEffect重新执行(封面加载中断并重启)。

使用Key优化后的代码

@Composable
fun MovieListWithKey(movies: List) {
    Column {
        movies.forEach { movie ->
            key(movie.id) { // 用电影唯一ID作为标识
                MovieItem(movie)
            }
        }
    }
}

此时,即使列表排序或增删,Compose 会通过movie.id识别相同实例,仅移动位置而不重组,LaunchedEffect也会继续执行未完成的任务。

二、jetpack compose的Effect

        Jetpack Compose 中,副作用指可组合函数范围外的应用状态变化。由于可组合项存在不可预测的重组、执行顺序变化等特性,直接在可组合项中处理副作用可能导致问题,因此 Compose 提供了一系列副作用 API,用于在受控环境中管理这些操作,确保其可预测性。

核心副作用 API 及用途

  • LaunchedEffect在可组合项生命周期内运行挂起函数。进入组合时启动协程,离开组合时取消协程;若传入的键值变化,会取消当前协程并启动新协程。适用于动画、延迟操作等需挂起函数的场景,例如通过延迟实现页面跳转。

@Composable
fun SplashScreen(navController: NavController) {
    // 延迟3秒后导航到主页
    LaunchedEffect(Unit) { // 键为Unit,仅在进入组合时启动一次
        delay(3000)
        navController.navigate("home")
    }
    
    // 显示启动页内容
    Box(modifier = Modifier.fillMaxSize()) {
        Text(text = "Loading...", modifier = Modifier.align(Alignment.Center))
    }
}

SplashScreen进入组合时,LaunchedEffect启动协程,延迟 3 秒后导航;离开组合时协程自动取消。

  • rememberCoroutineScope返回与调用点生命周期绑定的协程作用域,可在可组合项外部(如用户点击事件中)启动协程,离开组合时自动取消。常用于手动控制协程生命周期,例如点击按钮显示 Snackbar。

@Composable
fun ActionScreen() {
    val snackbarHostState = rememberSnackbarHostState()
    // 创建与当前组合绑定的协程作用域
    val scope = rememberCoroutineScope()

    Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
        Button(
            onClick = {
                // 点击时在作用域中启动协程
                scope.launch {
                    snackbarHostState.showSnackbar(
                        message = "操作成功",
                        duration = SnackbarDuration.Short
                    )
                }
            },
            modifier = Modifier.padding(padding)
        ) {
            Text("执行操作")
        }
    }
}

rememberCoroutineScope确保离开组合时,未完成的协程(如显示 Snackbar)会被取消。

  • rememberUpdatedState在效应中引用一个值,且该值变化时不会导致效应重启。适用于长期运行的效应需使用最新值的场景,例如延迟操作中需保持回调的最新状态,避免效应因值变化频繁重启。

@Composable
fun TimerScreen(onTimeOut: () -> Unit) {
    // 用rememberUpdatedState包装回调,确保效应中使用最新值
    val currentOnTimeOut by rememberUpdatedState(onTimeOut)

    // 效应仅在进入组合时启动一次(键为Unit)
    LaunchedEffect(Unit) {
        delay(5000) // 延迟5秒
        currentOnTimeOut() // 调用最新的回调
    }

    Text("倒计时中...")
}

即使onTimeOut参数变化,LaunchedEffect也不会重启,但currentOnTimeOut会始终指向最新回调。

  • SideEffect:每次重组成功后执行,用于将 Compose 状态同步到非 Compose 代码(如原生组件、分析库)。例如将当前用户类型同步到分析工具,确保后续事件携带最新元数据。

@Composable
fun UserProfileScreen(user: User) {
    // 初始化分析工具
    val analytics = remember { AnalyticsTool() }

    // 每次重组成功后,更新分析工具的用户属性
    SideEffect {
        analytics.setUserProperty("user_level", user.level.toString())
    }

    Text("用户等级:${user.level}")
}

确保分析工具始终使用最新的用户等级,且仅在重组成功后执行。

  • produceState:将非 Compose 状态(如 Flow、LiveData)转换为 Compose 状态。进入组合时启动协程推送值到返回的 State 中,离开组合时取消协程,适用于将外部状态引入组合,且值相同不会触发重组。

// 网络请求工具类
class DataRepository {
    suspend fun fetchData(): String {
        delay(2000) // 模拟网络延迟
        return "网络数据"
    }
}

@Composable
fun DataDisplayScreen() {
    // 将网络请求结果转换为Compose状态
    val dataState by produceState(
        initialValue = "加载中...", // 初始值
        repository = DataRepository() // 键:repository变化时重启
    ) {
        // 启动协程执行网络请求
        val data = repository.fetchData()
        value = data // 推送结果到State
    }

    Text(text = dataState)
}

produceState自动管理协程生命周期,离开组合时取消请求。

  • snapshotFlow:将 Compose 的 State 转换为 Flow,利用 Flow 运算符(如 distinctUntilChanged)处理状态变化。例如将滚动位置转换为 Flow,分析用户滚动行为。

场景:监听列表滚动位置,发送分析事件(利用 Flow 的运算符)。
@Composable
fun ScrollTrackingList() {
    val listState = rememberLazyListState()

    LazyColumn(state = listState) {
        items(100) { index ->
            Text("第${index + 1}项", modifier = Modifier.padding(8.dp))
        }
    }

    // 监听滚动位置,转换为Flow并处理
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .map { it > 10 } // 仅关注是否滚动过10项
            .distinctUntilChanged() // 去重,避免重复事件
            .filter { it } // 仅发送“已滚动过10项”的事件
            .collect {
                AnalyticsTool.sendEvent("scrolled_past_10_items")
            }
    }
}
  • snapshotFlow将滚动位置转换为 Flow,结合 Flow 运算符实现精准的事件跟踪。

重点扩展

        在 Jetpack Compose 中,derivedStateOf是一个非常实用的性能优化工具,特别适合处理高频变化状态但只需在特定条件下响应的场景。

1. 为什么需要 derivedStateOf

当列表滚动时,listState.firstVisibleItemIndex 会频繁变化(每滚动一帧就可能更新一次)。如果直接使用这个值来控制按钮显示,会导致以下问题:

// 未使用 derivedStateOf 的写法(性能问题)
val showTopButton = listState.firstVisibleItemIndex > 0

if (showTopButton) {
    Button(...) // 每次滚动都可能触发重组
}

 问题:即使按钮显示状态未变(如从 true 变为 true),Compose 也会执行重组,因为 firstVisibleItemIndex 的值一直在变化。

2. derivedStateOf 的优化原理

derivedStateOf 会创建一个派生状态,它只在结果值发生变化时才通知 Compose 进行重组。

// 使用 derivedStateOf 的写法(优化后)
val showTopButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

if (showTopButton) {
    Button(...) // 仅在 showTopButton 从 true 变 false 或反之触发重组
}

 工作流程

  1. 初始状态showTopButton = false(首项可见)。
  2. 滚动过程firstVisibleItemIndex 从 0 变为 1、2、3...,但 showTopButton 始终为 true不触发重组
  3. 临界变化:当 firstVisibleItemIndex 从 0 变为 1 时,showTopButton 从 false 变为 true触发一次重组(显示按钮)。
  4. 反向滚动:当 firstVisibleItemIndex 从 1 变回 0 时,showTopButton 从 true 变为 false触发一次重组(隐藏按钮)。

3.代码结构解析

val showTopButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

remember:确保 derivedStateOf 只在首次组合时创建,避免重复创建。

derivedStateOf

  • 输入:观察 listState.firstVisibleItemIndex 的变化。
  • 输出:基于条件 index > 0 计算出 showTopButton 的值。
  • 特性:自动使用 equals 比较新旧值,仅当结果变化时通知重组。

4.何时使用deriveStateOf?

适合以下场景:高频变化的状态(如滚动位置、动画值)。只需在特定条件下响应(如超过阈值、状态切换)。避免不必要的 UI 更新(如按钮显示 / 隐藏、文本截断)。

        阶段总结:derivedStateOf 是 Compose 中用于减少不必要重组的关键工具,通过结果去重机制,将高频变化的状态转换为低频更新的派生状态。在滚动场景中,它能将每秒数十次的重组减少到仅在临界状态变化时触发一次,显著提升性能。使用时需注意:仅在状态变化频率远高于 UI 更新需求时使用

三、jetpack compose的绘制阶段

        Compose 将数据转换为 UI 分为三个主要阶段,通常按组合、布局、绘制的顺序执行,形成单向数据流,不过 BoxWithConstraints、LazyColumn 等例外,其子项组合依赖父项布局阶段。

        组合阶段通过运行可组合函数生成表示 UI 的布局节点树,确定显示的 UI 内容;布局阶段包含测量和放置步骤,遍历 UI 树为每个节点在 2D 坐标中确定大小和位置,且一次遍历即可完成,性能高效;绘制阶段则从上到下遍历树,将每个节点渲染到 Canvas 上,最终形成屏幕上的 UI。

        Compose 会跟踪各阶段的状态读取,当状态值变化时,仅重新执行读取该状态的阶段相关操作,以此优化性能。

        状态读取可通过直接访问 value 或使用属性委托实现,不同阶段的状态读取影响不同:组合阶段读取的状态变化会触发重新组合,可能连带引发布局和绘制;布局阶段(测量或放置步骤)读取的状态变化仅触发布局和可能的绘制;绘制阶段读取的状态变化则只触发重绘。

        为提升性能,应将状态读取限制在尽可能低的阶段。例如,滚动时的偏移计算若放在布局阶段而非组合阶段,可避免每次滚动都重新组合,减少不必要的工作。同时,需避免 “重新组合循环”—— 即不同帧之间因阶段依赖形成的循环,这会导致 UI 跳动和额外开销,解决方法是使用合适的布局基元或自定义布局,让父级协调元素关系,而非通过动态状态传递尺寸信息。

代码示例:



// 综合示例:整合 Compose 三个阶段的核心用法
@Composable
fun ComposeThreePhasesDemo() {
    Column(modifier = Modifier.fillMaxSize()) {
        // 1. 组合阶段示例(动态内容 + derivedStateOf 优化)
        CompositionPhaseSection()
        
        // 2. 布局阶段示例(动态偏移 + 自定义布局)
        LayoutPhaseSection()
        
        // 3. 绘制阶段示例(动态绘制 + 绘制修饰符)
        DrawingPhaseSection()
    }
}

// 1. 组合阶段:控制显示内容,状态变化触发重组
@Composable
private fun CompositionPhaseSection() {
    var clickCount by remember { mutableIntStateOf(0) }
    // 优化:使用 derivedStateOf 过滤高频状态变化
    val showToast by remember {
        derivedStateOf { clickCount % 5 == 0 && clickCount > 0 }
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .border(2.dp, Color.Gray, shape = MaterialTheme.shapes.medium)
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "组合阶段示例",
            fontSize = 18.sp,
            fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        
        // 组合阶段读取状态:clickCount 变化触发重组
        Text(
            text = "点击次数: $clickCount",
            fontSize = 16.sp
        )
        
        if (showToast) { // 仅在 clickCount 是 5 的倍数时显示
            Text(
                text = "每点击 5 次显示一次",
                color = Color.Red,
                modifier = Modifier.padding(8.dp)
            )
        }
        
        Button(
            onClick = { clickCount++ },
            modifier = Modifier.padding(top = 8.dp)
        ) {
            Icon(Icons.Default.Add, contentDescription = "增加")
            Text("点击增加", modifier = Modifier.padding(start = 4.dp))
        }
    }
}

// 2. 布局阶段:控制位置,状态变化仅触发布局
@Composable
private fun LayoutPhaseSection() {
    val listState = rememberLazyListState()
    var spacing by remember { mutableStateOf(8.dp) } // 控制自定义布局间距

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .border(2.dp, Color.Blue, shape = MaterialTheme.shapes.medium)
            .padding(16.dp)
    ) {
        Text(
            text = "布局阶段示例",
            fontSize = 18.sp,
            fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        
        // 布局阶段状态读取:滚动偏移仅触发布局(视差效果)
        Box(modifier = Modifier.height(150.dp)) {
            Image(
                painter = painterResource(id = R.drawable.ic_launcher_background), // 替换为实际图片
                contentDescription = "视差背景",
                modifier = Modifier
                    .fillMaxWidth()
                    .height(150.dp)
                    .offset {
                        // 布局阶段(放置步骤)读取状态:仅重布局
                        IntOffset(
                            x = 0,
                            y = -listState.firstVisibleItemScrollOffset / 3 // 视差效果
                        )
                    }
            )
        }
        
        // 自定义布局:手动测量和放置子元素
        Text("自定义布局(间距可调整)", modifier = Modifier.padding(top = 16.dp))
        CustomVerticalLayout(
            spacing = spacing,
            modifier = Modifier.padding(vertical = 8.dp)
        ) {
            Text("子元素 1", modifier = Modifier.background(Color.LightGray).padding(4.dp))
            Text("子元素 2", modifier = Modifier.background(Color.LightGray).padding(4.dp))
            Text("子元素 3", modifier = Modifier.background(Color.LightGray).padding(4.dp))
        }
        
        Row(verticalAlignment = Alignment.CenterVertically) {
            Text("间距: ${spacing.value}dp", modifier = Modifier.width(80.dp))
            Slider(
                value = spacing.value,
                onValueChange = { spacing = it.dp },
                valueRange = 4f..32f
            )
        }
        
        // 滚动列表(触发视差效果)
        Text("滚动列表触发视差", modifier = Modifier.padding(top = 16.dp))
        LazyColumn(
            state = listState,
            modifier = Modifier
                .height(120.dp)
                .border(1.dp, Color.Gray),
            contentPadding = PaddingValues(8.dp)
        ) {
            items(20) { index ->
                Text("列表项 $index", modifier = Modifier.padding(8.dp))
            }
        }
    }
}

// 自定义布局实现(布局阶段核心逻辑)
@Composable
private fun CustomVerticalLayout(
    spacing: Dp,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 1. 测量所有子元素
        val placeables = measurables.map { it.measure(constraints) }
        
        // 2. 计算总高度(子元素高度 + 间距总和)
        val totalHeight = placeables.sumOf { it.height } + 
                (spacing.roundToPx() * (placeables.size - 1)).coerceAtLeast(0)
        
        // 3. 放置子元素(垂直排列)
        layout(constraints.maxWidth, totalHeight) {
            var yPosition = 0 // 初始 Y 坐标
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition) // 放置子元素
                yPosition += placeable.height + spacing.roundToPx() // 更新 Y 坐标
            }
        }
    }
}

// 3. 绘制阶段:控制渲染,状态变化仅触发重绘
@Composable
private fun DrawingPhaseSection() {
    var progress by remember { mutableFloatStateOf(0.2f) } // 进度值(0-1)
    var rectColor by remember { mutableStateOf(Color.Cyan) } // 矩形颜色

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .border(2.dp, Color.Green, shape = MaterialTheme.shapes.medium)
            .padding(16.dp)
    ) {
        Text(
            text = "绘制阶段示例",
            fontSize = 18.sp,
            fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        
        // 绘制修饰符:在背景绘制进度条
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(40.dp)
                .background(Color.LightGray)
                .drawBehind {
                    // 绘制阶段读取进度:仅触发重绘
                    val progressWidth = size.width * progress
                    drawRect(
                        color = rectColor,
                        size = Size(progressWidth, size.height)
                    )
                }
        )
        
        // 控制进度的滑块
        Slider(
            value = progress,
            onValueChange = { progress = it },
            modifier = Modifier.padding(vertical = 16.dp)
        )
        
        // 动态绘制示例(Canvas)
        Text("自定义 Canvas 绘制", modifier = Modifier.padding(top = 16.dp))
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .border(1.dp, Color.Gray)
        ) {
            // 绘制阶段读取状态:颜色和进度变化仅触发重绘
            drawCircle(
                color = rectColor,
                radius = size.minDimension / 4 * (1 + progress), // 半径随进度变化
                center = Offset(size.width / 2, size.height / 2)
            )
        }
        
        // 切换颜色按钮
        Button(
            onClick = {
                rectColor = listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow).random()
            },
            modifier = Modifier.padding(top = 16.dp)
        ) {
            Text("随机切换颜色")
        }
    }
}

// 预览
@Preview(showBackground = true, name = "Compose 三阶段综合示例")
@Composable
fun ComposeThreePhasesDemoPreview() {
    MaterialTheme {
        ComposeThreePhasesDemo()
    }
}

阶段总结

        Compose 将数据转换为 UI 分为组合、布局、绘制三个主要阶段,通常按此顺序形成单向数据流(BoxWithConstraints、LazyColumn 等例外,其子项组合依赖父项布局阶段):组合阶段通过运行可组合函数生成 UI 布局节点树以确定显示内容,布局阶段经测量和放置步骤为 UI 树节点确定 2D 坐标中的大小和位置(一次遍历高效完成),绘制阶段从上到下遍历树将节点渲染到 Canvas;

        Compose 会跟踪各阶段的状态读取(可通过直接访问 value 或属性委托实现),状态变化时仅重新执行对应阶段操作以优化性能 —— 组合阶段的状态变化触发重新组合及可能的后续阶段,布局阶段的仅触发布局和可能的绘制,绘制阶段的只触发重绘;为提升性能,应将状态读取限制在尽可能低的阶段(如滚动偏移计算放在布局阶段),同时需避免不同帧间因阶段依赖形成的 “重新组合循环”(会导致 UI 跳动和额外开销),解决方法是使用合适的布局基元或自定义布局,由父级协调元素关系而非通过动态状态传递尺寸信息

你可能感兴趣的:(Android-Compose初步学习总结)