Compose 生产级配置:从 demo 到可上线

12 阅读6分钟

目录

1. 引言

Jetpack Compose 上手容易,但把应用真正推向生产环境时,往往会遇到一系列「demo 里不会遇到」的问题:性能调优、稳定性保障、可测试性、包体积控制、崩溃治理……本文从实际生产经验出发,梳理 Compose 应用从 demo 走向可上线需要关注的关键配置与实践,帮助你少踩坑、快上线。

TL;DR:本文从构建、性能、稳定性、测试、包体积五个维度,梳理 Compose 应用从 demo 走向可上线的关键配置。构建上锁定 Compose 编译器与 Kotlin 版本并开启 R8 混淆;性能上开启强跳过模式、合理使用 remember / derivedStateOf;稳定性上用 LaunchedEffect 管理副作用并接入监控;测试上为核心交互编写 UI 用例;包体积上用 Baseline Profiles 与按需加载优化启动与体积。

2. 构建与依赖配置

2.1 启用 Compose 编译器

生产环境必须锁定 Compose 编译器版本,并与 Kotlin 版本严格对应:

// 根 build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"
}

// 模块 build.gradle.kts
android {
    buildFeatures {
        compose = true
    }
}

composeOptions {
    kotlinCompilerExtensionVersion = "1.5.14"
}

注意:Compose 编译器版本与 Kotlin 版本存在强绑定关系,升级 Kotlin 时必须同步升级编译器插件,否则会出现编译错误或运行时异常。

2.2 开启 R8 混淆与资源收缩

生产构建建议开启代码混淆与资源收缩,显著减小包体积:

buildTypes {
    release {
        isMinifyEnabled = true
        isShrinkResources = true
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}

Compose 相关混淆规则通常已由官方库自带,但自定义 Composable 若使用了反射或序列化,需要补充 keep 规则。

2.3 生产环境 vs Demo 环境配置对比

下表汇总了 Compose 项目从 Demo 走向生产时,各项关键配置的差异,便于快速对照落地:

配置项Demo 环境生产环境说明
Compose 编译器版本跟随最新版本,灵活升级锁定固定版本,与 Kotlin 严格对应生产环境必须保证编译可复现,避免升级 Kotlin 时编译器不兼容
R8 混淆与资源收缩通常关闭,便于调试开启 isMinifyEnabled 与 isShrinkResources显著减小包体积,但需补充自定义 Composable 的 keep 规则
强跳过模式(Strong Skipping)可关闭,便于观察重组开启 enableComposeCompilerStrongSkipping减少不必要的重组,对列表、复杂 UI 场景收益明显
Baseline Profiles无需配置生成并打包进 APK提升冷启动与列表滚动性能,建议在发布前生成
稳定性监控不接入接入 Crashlytics 或自建上报生产环境需对重组异常、协程取消等 Compose 特有崩溃做专项采集
UI 测试可选,少量冒烟用例核心交互全覆盖,使用稳定 testTag保证发布质量,避免回归

小结:Demo 追求「跑得快、看得见效果」,生产则追求「稳定、可复现、可观测」。上表所列配置项,建议在发布前逐项核对,避免带着 Demo 配置直接上线。

3. 性能配置

3.1 开启强跳过模式(Strong Skipping)

Compose 编译器 1.5.4+ 支持强跳过模式,可显著减少不必要的重组:

// gradle.properties
android.experimental.enableComposeCompilerStrongSkipping=true

该模式让编译器对参数未变化的 Composable 自动跳过重组,对列表、复杂 UI 场景收益明显。

3.2 合理使用 remember 与 derivedStateOf

生产代码中应避免在 Composable 内重复计算昂贵逻辑:

@Composable
fun ProductList(products: List<Product>, query: String) {
    // 用 derivedStateOf 避免每次重组都重新过滤
    val filtered = remember(products, query) {
        derivedStateOf { products.filter { it.name.contains(query) } }
    }
    LazyColumn {
        items(filtered.value, key = { it.id }) { product ->
            ProductItem(product)
        }
    }
}

实战:搜索框防抖

搜索场景是防抖的典型应用:用户连续输入时不应每次都触发网络请求,而应在输入停顿后再搜索。下面用 remember 保存防抖状态、LaunchedEffect 配合 delay 实现自动搜索:

@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    // 1. 用 remember 保存输入框的当前文本(防抖状态)
    var query by remember { mutableStateOf("") }
    // 2. 保存搜索结果与加载状态
    val results by viewModel.results.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        OutlinedTextField(
            value = query,
            onValueChange = { query = it },  // 只更新本地状态,不直接触发搜索
            placeholder = { Text("输入关键词搜索") },
            modifier = Modifier.fillMaxWidth()
        )

        // 3. LaunchedEffect 监听 query 变化,配合 delay 实现防抖
        LaunchedEffect(query) {
            // 输入停顿 500ms 后才真正发起搜索
            delay(500)
            viewModel.search(query.trim())
        }

        // 4. 加载状态展示
        if (isLoading) {
            CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
        } else if (results.isEmpty()) {
            // 5. 空结果处理
            Text(
                text = "未找到相关结果",
                modifier = Modifier.align(Alignment.CenterHorizontally)
            )
        } else {
            LazyColumn {
                items(results, key = { it.id }) { item ->
                    SearchResultItem(item)
                }
            }
        }
    }
}

关键点说明:

  • remember 只保存输入文本,不触发搜索,避免每次重组都发起请求;
  • LaunchedEffect(query) 在 query 变化时重启协程,配合 delay(500) 实现「停顿后才搜索」;
  • 用户连续输入时,前一个协程会被自动取消,只有最后一次停顿后的输入才会真正发起搜索;
  • 加载状态与空结果分别用 CircularProgressIndicator 和提示文案展示,交互更完整。

3.3 列表使用 key 提升复用效率

LazyColumn / LazyRow 中务必为 items 指定稳定的 key,避免滚动时 Item 状态错乱:

items(items = list, key = { it.id }) { item -> ... }

4. 稳定性与崩溃治理

4.1 配置稳定性监控

生产环境建议接入稳定性监控,例如 Firebase Crashlytics 或自建上报通道,并针对 Compose 特有的崩溃场景(如重组异常、协程取消)做专项采集。

4.2 避免在 Composition 中执行副作用

不要在 Composable 函数体内直接执行网络请求、数据库读写等耗时操作,应使用 LaunchedEffect 或 rememberCoroutineScope 管理副作用:

@Composable
fun UserProfile(userId: String) {
    var state by remember { mutableStateOf<UiState>(Loading) }
    LaunchedEffect(userId) {
        state = repository.fetchUser(userId)
    }
    // 渲染 UI
}

4.3 处理协程取消与资源释放

页面销毁时协程应自动取消,避免内存泄漏:

@Composable
fun DetailScreen(viewModel: DetailViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // 使用 collectAsStateWithLifecycle 而非 collectAsState,避免后台收集
}

5. 可测试性配置

5.1 引入 Compose UI 测试依赖

androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.6")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.6")

5.2 编写稳定的 UI 测试

生产级项目应为核心交互编写 UI 测试,并设置稳定的测试标签:

@Composable
fun LoginButton(onClick: () -> Unit) {
    Button(onClick = onClick, modifier = Modifier.testTag("login_button")) {
        Text("登录")
    }
}
@RunWith(AndroidJUnit4::class)
class LoginScreenTest {
    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun clickLogin_showsLoading() {
        composeRule.setContent { LoginScreen() }
        composeRule.onNodeWithTag("login_button").performClick()
        composeRule.onNodeWithText("加载中").assertIsDisplayed()
    }
}

6. 包体积与启动优化

6.1 使用 Baseline Profiles

Baseline Profiles 可显著提升冷启动与列表滚动性能:

// 模块 build.gradle.kts
plugins {
    id("androidx.baselineprofile")
}

生成后放入 src/main/baselineProfiles/ 目录,并在构建配置中引用。

6.2 按需加载与动态特性

对体积较大的功能模块,可考虑使用 Dynamic Feature 按需加载,减少首包体积。

7. 总结

从 demo 到可上线,Compose 项目需要在构建、性能、稳定性、测试、包体积五个维度做系统性配置。核心要点:

  • 锁定 Compose 编译器与 Kotlin 版本,开启 R8 混淆;
  • 开启强跳过模式,合理使用 remember / derivedStateOf;
  • 用 LaunchedEffect 管理副作用,接入稳定性监控;
  • 引入 UI 测试并编写核心交互用例;
  • 使用 Baseline Profiles 优化启动与滚动性能。

配置到位后,Compose 应用才能在生产环境中稳定、流畅地运行。希望本文能帮你少走弯路,顺利上线。