You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Compose Navigation与ViewModel生命周期异常问题咨询

Jetpack Compose 导航与ViewModel生命周期异常问题

我刚接触Compose时,初看之下它和我非常喜欢的SwiftUI十分相似,但实际投入使用后很快遇到了诸多问题,我需要找到正确的使用方式才能发挥其能力,以下是我遇到的问题之一:

package org.test.android.kotlin.compose.ui

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.test.android.kotlin.compose.ui.theme.MbiKtTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MbiKtTheme {
                val navController = rememberNavController()
                // <Edit #1>
                // Navigator.route.collectAsState("").value.takeIf { it.isNotEmpty() }?.also { navController.navigate(it) }
                // Navigator.route.observe(this, { route -> navController.navigate(route) })
                // </Edit #1>
                // <Edit #2>
                Navigator.route.collectAsState("").value.takeIf { it.isNotEmpty() }?.also { 
                    navController.popBackStack()
                    navController.navigate(it)
                }
                // </Edit #2>
                Surface(color = MaterialTheme.colors.background) {
                    NavHost(
                        navController = navController,
                        startDestination = "setup"
                    ) {
                        composable(route = "setup") {
                            SetupScreen()
                        }
                        composable(route = "progress") {
                            ProgressScreen()
                        }
                    }
                }
            }
        }
    }
}

// 该实现当前简单示例中并非必要,但在大型模块化项目中是必须的
object Navigator {
    // <Edit #1>
    val route = MutableSharedFlow<String>(0, 1, BufferOverflow.DROP_OLDEST)
    //val route: MutableLiveData<String> = MutableLiveData()
    // </Edit #1>
}

class SetupViewModel : ViewModel() {
    init {
        Log.d(toString(), "Create")
    }

    override fun onCleared() {
        Log.d(toString(), "Destroy")
    }

    override fun toString(): String {
        return "SetupViewModel"
    }
}

@Composable
fun SetupScreen(model: SetupViewModel = viewModel()) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(all = Dp(8f))
    ) {
        Text(text = "Setup")
        Spacer(modifier = Modifier.weight(1f))
        Button(onClick = { Navigator.route.tryEmit("progress") }, modifier = Modifier.fillMaxWidth()) { Text(text = "Register") }
    }
}

class ProgressViewModel : ViewModel() {
    init {
        Log.d(toString(), "Created")
    }

    override fun onCleared() {
        Log.d(toString(), "Cleared")
    }

    override fun toString(): String {
        return "ProgressViewModel"
    }
}

@Composable
fun ProgressScreen(model: ProgressViewModel = viewModel()) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(all = Dp(8f))
    ) {
        Text(text = "Progress")
        Spacer(modifier = Modifier.weight(1f))
        Button(onClick = { Navigator.route.tryEmit("setup") }, modifier = Modifier.fillMaxWidth()) { Text(text = "Abort") }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MbiKtTheme {
        SetupScreen()
    }
}

问题复现与描述

  • 在两个Compose页面之间导航并旋转屏幕,观察LogCat中两个ViewModel的创建、销毁日志
  • 问题一:从一个页面导航到另一个页面时,前一个页面的ViewModel从未回调onCleared销毁(显然因为Activity仍存活),这在大型项目中是完全不可接受的
  • 问题二:只要完成过一次页面跳转,每次旋转屏幕ViewModel都会被重新创建,这同样不可接受

我了解Compose目前尚未完全成熟,部分组件仍处于alpha版本,不确定这是Compose本身的Bug,还是我对大型模块化项目中Compose的使用方式存在误解,希望得到解决方案。

问题更新

  • 补充1(2021/09/05):通过相关文章指引,我已修复旋转屏幕时ViewModel重建的问题(暂不清楚背后原理),目前仅剩ViewModel生命周期不符合预期的问题。
  • 补充2(2021/09/13):通过回答指引,我已实现ViewModel生命周期符合预期的效果,我禁用了回退栈,该功能在我的业务场景中本身不需要,还会导致UI和底层模型逻辑混乱。

内容的提问来源于stack exchange,提问作者Jiří Křivánek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 13:24:05