在 Android 开发中,Jetpack Compose 已经成为了构建 UI 的主流方式,它用声明式编程彻底改变了我们写界面的习惯。但实际项目里,我们不可能只用 Compose 自己的组件,几乎每个 App 都要跟网络请求、图片加载、数据库、导航这些第三方库打交道。这些库原本是为传统 View 系统设计的,把它们和 Compose 无缝搭配起来,中间有不少坑要填。今天我就用纯大白话,结合我自己踩过的坑,给你掰扯清楚整个过程中的注意事项和常见问题,保证你读完能少走弯路。

一、为什么要专门聊集成这件事

Compose 的 UI 是函数式的,状态变化自动触发重绘,而大多数第三方库都是基于回调或者 LiveData 这种传统的观察者模式。两种思维模式撞到一起,如果不注意适配,很容易出现界面不刷新、内存泄漏、或者莫名其妙崩溃的情况。比如你用 Retrofit 发请求拿到数据,想直接显示在 Compose 的 Text 上,如果代码写得不对,数据回来了界面却纹丝不动。所以搞清楚怎么桥接这两套体系,是每个 Compose 开发者必须迈过的坎。

二、集成的应用场景举例

网络请求:最典型的就是 Retrofit + OkHttp。在 Compose 里我们通常配合 ViewModel 和 StateFlow 来管理数据流。
图片加载:Coil 是目前比较推荐的图片库,它对 Compose 有原生支持,但也有要注意的细节。
导航:官方 Navigation Compose 库虽然是自家产品,但和 BottomNavigation、DeepLink 配合时容易踩坑。
本地存储:Room 数据库配合 Flow 在 Compose 里更新数据看起来很丝滑,但生命周期方面要格外小心。
依赖注入:Hilt 或者 Koin 配合 Compose 时,作用域管理不当会导致 UI 被注入无用对象。

下面我们一个个来看,怎么集成才稳。

三、集成时的注意事项

3.1 生命周期管理要放在第一位

传统 View 系统里我们经常在 Activity/Fragment 的 onDestroy 里取消请求。Compose 里没有这些回调,你得靠 LaunchedEffectDisposableEffect 来手动管理。比如发起网络请求时,页面关掉了就应该取消,否则浪费带宽还可能崩溃。

// 技术栈:Kotlin + Jetpack Compose + Retrofit

@Composable
fun UserScreen(userId: String) {
    // 使用 LaunchedEffect 在 Composable 进入组合时启动协程
    LaunchedEffect(userId) {
        // 假设 viewModel 已经在外面创建好
        viewModel.loadUser(userId) // 这个函数内部会控制生命周期
    }
    // 显示用户信息...
}

ViewModel 里通常这样写:

// 技术栈:Kotlin
class UserViewModel : ViewModel() {
    private val _userState = MutableStateFlow<User?>(null)
    val userState: StateFlow<User?> = _userState.asStateFlow()

    fun loadUser(userId: String) {
        viewModelScope.launch {
            try {
                val user = retrofitService.getUser(userId)
                _userState.value = user
            } catch (e: Exception) {
                // 处理错误
            }
        }
    }
}

关键点:viewModelScope 会在 ViewModel 被清除时自动取消协程,所以不会漏掉。

3.2 状态订阅要使用 Compose 的集合 API

很多第三方库返回的数据是 LiveData 或者普通回调,你不能直接把它们赋值给 Compose 的状态。必须转成 State 或者 StateFlow 才行。好在 Jetpack 提供了 livedata(), stateFlow() 这些扩展函数,但注意使用时要放在合适的 collectAsState() 里。

// 技术栈:Kotlin + Jetpack Compose
@Composable
fun BookListScreen(viewModel: BookViewModel) {
    // 假设 viewModel.books 是 StateFlow<List<Book>>
    val books by viewModel.books.collectAsState()

    LazyColumn {
        items(books) { book ->
            Text(text = book.title)
        }
    }
}

如果你用的是 LiveData,可以这样:

// 技术栈:Kotlin + Jetpack Compose
@Composable
fun LiveDataExample(liveData: LiveData<String>) {
    val value by liveData.observeAsState() // 注意:需要 lifecycle-runtime-compose 依赖
    Text(text = value ?: "Loading...")
}

这里有个坑:observeAsState() 需要传入 LifecycleOwner,通常在 Composable 里可以直接用 LocalLifecycleOwner.current,但如果你嵌套了 Fragment 或者 Activity 的 lifecycle 不对齐,容易丢更新。

3.3 图片加载库的最佳实践

Coil 对 Compose 的支持已经很好,直接用 AsyncImage 就行。但要注意内存缓存和图片大小裁剪,否则列表滑动时容易卡顿。

// 技术栈:Kotlin + Jetpack Compose + Coil
@Composable
fun ProductImage(url: String) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .crossfade(true)
            .size(300) // 限制尺寸减少内存占用
            .build(),
        contentDescription = "商品图片",
        modifier = Modifier.fillMaxWidth(),
        placeholder = painterResource(R.drawable.placeholder),
        error = painterResource(R.drawable.error)
    )
}

如果出现图片闪烁或者重复加载,检查一下 key 是否设置正确。Coil 的 ImageRequest 默认会根据 URL 做缓存,但如果 URL 没变内容变了(比如 CDN 更新了),你需要手动添加 memoryCachePolicy 或者改变 key

3.4 导航库的 Fragment 兼容与深度链接

官方 Navigation Compose 库已经比较成熟,但当你需要和旧的 Fragment 共存时,就得用 NavHost 配合 composable 路由。要注意给每个路由定义好参数和启动模式。

// 技术栈:Kotlin + Jetpack Compose Navigation
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { id -> navController.navigate("detail/$id") }
            )
        }
        composable(
            route = "detail/{id}",
            arguments = listOf(navArgument("id") { type = NavType.StringType })
        ) { backStackEntry ->
            val id = backStackEntry.arguments?.getString("id") ?: return@composable
            DetailScreen(id = id)
        }
    }
}

深度链接(DeepLink)需要额外配置 AndroidManifest.xml 里的 intent-filter,并且在 NavHost 里设置 deepLinks。容易出问题的是参数解析,如果 URL 里包含编码字符,记得用 URLDecoder 解码。

四、常见问题与解决方案

4.1 状态不更新,UI 还是旧数据

这是新手最容易遇到的问题。如果你在 ViewModel 里修改了某个可变变量,但忘记用 StateFlow 或者 mutableStateOf 包裹,Compose 就不会知道数据变了。解决办法:所有 UI 依赖的数据必须用 Compose 认得的状态容器,比如 State, MutableState, StateFlow

// 错误示例
class BadViewModel : ViewModel() {
    var name = "" // 改成这样无法触发重组
}

// 正确示例
class GoodViewModel : ViewModel() {
    private val _name = MutableStateFlow("")
    val name: StateFlow<String> = _name.asStateFlow()
}

4.2 页面关闭后协程还在跑

如果你在 Composable 里直接用 rememberCoroutineScope() 启动协程,但没有在页面退出时取消,那么网络请求可能会在后台继续执行,甚至更新已经不存在的 UI 导致崩溃。强烈推荐使用 LaunchedEffect,它会自动跟随 Composable 的生命周期。

// 避免在 Composable 中这样写
@Composable
fun DangerousScreen() {
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            // 网络请求如果页面关闭后还在跑
        }
    })
}

改用 LaunchedEffect(key) 或者把网络请求放在 ViewModel 的 viewModelScope 里。

4.3 使用 Hilt 注入时出现重复创建 ViewModel

Hilt 默认会给每个 Activity 或者 Fragment 创建独立的 ViewModel 实例。如果你在 Composable 里用 hiltViewModel() 并且嵌套了多个 composable 函数,可能会意外创建多个 ViewModel。解决方法是保证 hiltViewModel() 只在最外层的 composable 里调用一次,然后通过参数传给子 composable。

// 在 NavHost 的 composable 外部创建
@Composable
fun App() {
    val navController = rememberNavController()
    val sharedViewModel: SharedViewModel = hiltViewModel() // 只创建一次
    NavHost(...) {
        composable("screenA") {
            ScreenA(viewModel = sharedViewModel)
        }
    }
}

4.4 Coil 图片加载慢或加载失败

可能是没有处理 placeHoldererror,导致图片加载时白屏或者一直转圈。另一个常见原因是网络图片的证书问题,比如自签名证书,Coil 默认使用 OkHttp 的配置,你需要自定义 ImageLoader 加载信任所有证书。当然生产环境不建议这样做。

// 自定义 ImageLoader 示例
@Composable
fun rememberImageLoader(): ImageLoader {
    val context = LocalContext.current
    return remember {
        ImageLoader.Builder(context)
            .okHttpClient(
                OkHttpClient.Builder()
                    .connectTimeout(30, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .build()
            )
            .build()
    }
}

然后通过 ImageLoader 传递给 AsyncImage

4.5 Room Flow 在 Compose 里收集时的生命周期问题

Room 返回的 Flow 是冷流,在 Compose 里用 collectAsState() 收集时,如果 Compose 没有在屏幕上(比如被 NavHost 切走了),Flow 依然可能会发射数据,但 UI 不消费,造成无谓的数据库读取。理想做法是在 ViewModel 里结合 stateIn 或者 shareIn 来控制作用域。

// ViewModel 中
class ProductViewModel : ViewModel() {
    private val db = AppDatabase.getInstance()
    
    val products: StateFlow<List<Product>> = db.productDao().getAllProducts()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000), // 5秒后取消
            initialValue = emptyList()
        )
}

这样当 UI 不再订阅时,数据库查询会在 5 秒后自动停止,避免后台空跑。

五、总结

Jetpack Compose 与第三方库集成并不是简单地贴上代码就完事,最大的挑战在于状态管理和生命周期对齐。记住三个核心原则:

  1. 所有 UI 用的数据必须用 Compose 认识的状态包装(StateFlow、MutableState、LiveData 加 observeAsState)。
  2. 协程任务一定要绑定到可取消的作用域,优先用 ViewModelScope 或 LaunchedEffect。
  3. 初始化库实例时注意上下文和依赖注入的作用域,避免重复创建或内存泄漏。

另外,官方推荐优先选择那些已经主动适配 Compose 的库(比如 Coil、Retrofit 配合 Coroutines、Hilt 配合 hiltViewModel),这样能减少很多麻烦。如果遇到老库,你就需要用 AndroidView 或者 interop 来包裹,但那又是另一个话题了。希望今天这篇能让你在集成路上走得顺一些,有问题多查官方文档,别自己硬猜。