先问几个问题:
在使用传统的Activity+Fragment的时候,程序进入后台或者前台的时候会调用对应的生命周期,那么如果我们全部换成Compose以后,会如何呢?我们需要如何做?
Activity 任务栈 和Compose的Navhost有什么关系?比如compose的页面如何后退,如何跳转,如何传递数据等等。
Compose 从前台退到后台,再进入前台,那么会重组?还是会怎么?
在刚开始开发的时候,我们就会有这些疑问,今天我们就通过一个实战例子来解释一下。
1.1 WanAndroid的例子
本期,我们用的纯compose,来模仿一下Activity+Fragment

大致代码如下 (1)
kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeWanAndroidTheme {
// 创建导航控制器
val navController = rememberNavController()
// 提供导航控制器给整个应用
CompositionLocalProvider(LocalNavController provides navController) {
// 主应用组件
WanApp(navController)
}
}
}
}
kt
@Composable
fun WanApp(navController: NavHostController) {
NavHost(navController = navController, startDestination = RoutePath.main) {
composable(route = RoutePath.main) {//主页
MainScaffold(navController)
}
composable(route = RoutePath.login) {//登录页
LoginCompose(navController)
}
}
}
一、NavHostController:持有整个应用的导航状态(当前在哪个页面、后台的页面栈等)到时我们需要他做如下功能:
(1)比如返回:navController.navigateUp()
-> 向上导航(类似于返回)。
(2)比如跳转:navController.navigate("login")
-> 跳转到登录页。
二、NavHost :算是一个导航的容器吧,可以暂时把他类比为一个activity,然后composable(route = RoutePath.main)类似于Fragment。比如现在就是一个Activity+两个Fragment。他的内部会根据 navController
当前指示的目的地,来显示对应的页面内容。 (1) navController = navController
:告诉这个 NavHost
要听从哪个控制器的指挥。
(2)startDestination = RoutePath.main
:指定起始页面(或称主页)。当应用打开或栈为空时,就显示这个页面。RoutePath.main
通常是一个字符串常量,比如 "main"
。
三、Composable :他的作用是注册页面。它告诉导航库:"当导航到 'login'
这个地址时,请执行我括号里的 lambda 表达式,并把 LoginCompose
这个界面显示出来"。
那么当我们执行这两个页面跳转是如何操作呢?这就需要我们使用一下NavHostController的操作。
1.2 NavHostController的页面跳转
kt
Text("测试页面跳转", modifier = Modifier.clickable {
//进行页面跳转,点击返回键,也可以返回。
navHostController.navigate(RoutePath.login)
})
kt
@Composable
fun LoginCompose(navHostController: NavHostController){
Scaffold{innerPadding->
Text("login页面", modifier = Modifier.clickable {
//返回,可以看到其实他是对我们的返回函数,做了拦截处理。
navHostController.navigateUp()
}.padding(innerPadding))
}
}
就这两句代码,我们就可以实现页面的切换和返回。
那么问一个问题,这里的页面跳转,是否会和activity一样,会执行一些onStop,或者返回的时候执行onResume方法?
其实这里和Activity还不太一样。我们先说,会不会重组。
其实是不会的。从 Main
页跳到 Login
页,再按返回键回到 Main
页。Main
页会恢复它之前的状态,而不会重新初始化。这是因为导航库自动管理了后台栈,之前的页面只是被保存在了后台,并没有被销毁。重组还是遵守之前的原则,当可观察的状态发生变化时才会执行。
那么如果我想类似于Activity一样,在页面切换的时候,执行一下onstop,返回的时候执行onresume呢?或者compose里面的写法是如何的?
在 Compose 中,我们通过副作用(Side Effects) API 来模拟和响应这种"进入/退出"组件树的行为。
LaunchedEffect
- 模拟onStart
/onResume
scss
var timerValue by remember { mutableStateOf(0) }
var isTimerActive by remember { mutableStateOf(true) }
// 效果1:使用 LaunchedEffect 根据 isTimerActive 状态决定是否启动计时器
LaunchedEffect(key1 = isTimerActive) {
// 只有当 isTimerActive 为 true 时,才启动这个协程
if (isTimerActive) {
while (true) {
delay(1000L) // 每秒一次
timerValue++
}
}
// 当 isTimerActive 变为 false,或者此Composable退出组合时,协程会自动取消
}
DisposableEffect
- 模拟onStop
/onDestroy
go
// 效果2:使用 DisposableEffect 来响应页面的"进入"和"退出"
// 它非常适合执行订阅和清理操作
DisposableEffect(Unit) { // key 是 Unit,意味着这个效果只在整个页面的生命周期内执行一次
// onStart (相当于 onResume)
println("MainScaffold: Composed. Starting expensive resource...")
isTimerActive = true // 页面可见,启动计时器
// 你必须返回一个 onDispose 闭包,它会在页面退出时调用
onDispose {
// onStop (相当于 onPause)
println("MainScaffold: Disposed. Releasing expensive resource...")
isTimerActive = false // 页面不可见,停止计时器
// 这里还可以做其他清理工作,比如取消网络请求、解除广播监听等
}
}
执行逻辑:
- 进入Main页时,
DisposableEffect
执行,打印日志并将isTimerActive
设为true
。LaunchedEffect
检测到isTimerActive
变为true
,启动计时协程。 - 跳转到
Login
页 :MainScaffold
从 Composable 树中被移除 。DisposableEffect
的onDispose
块被调用,打印日志并将isTimerActive
设为false
。LaunchedEffect
的协程因为其key
(isTimerActive
) 变化或 Composable 退出,会自动取消。 - 返回到main页,
MainScaffold
被重新组合到 Composable 树中。整个过程从第 1 步开始重复。
总结一下就是:
概念 | Activity 生命周期 | Compose 方式(等效实现) |
---|---|---|
页面创建/进入 | onCreate() -> onStart() -> onResume() |
DisposableEffect { ...; onDispose{} } 内部代码 + LaunchedEffect |
页面退出/不可见 | onPause() -> onStop() |
DisposableEffect 的 onDispose 块 |
页面被系统销毁 | onDestroy() |
onDispose 块(但 Compose 导航默认保持后台栈,通常不会销毁) |
那么我想知道,这些页面的viewmodel又是如何使用的?
1.3 compose中使用viewmodel,他的生命周期如何,作用域如何
首先要理解一个关键点:导航库将每个目的地(composable
)与一个 NavBackStackEntry
关联起来 。这个 NavBackStackEntry
就是 ViewModel 的作用域单位。
-
默认情况(不共享) :当你导航到一个新页面时,会创建一个新的
NavBackStackEntry
。在此作用域内获取的 ViewModel 是全新的、独立的 ,与其他页面的 ViewModel 不共享。 -
返回时 :当你从新页面返回,该页面对应的
NavBackStackEntry
及其 ViewModel 会被保留在后台栈中(直到被弹出)。因此,返回时页面状态(如输入框文本、列表滚动位置)得以保留,正是得益于 ViewModel 的存活。 -
永久离开时 :当你通过
popBackStack
或navigate(...) { popUpTo(...) }
将页面从后台栈中永久移除 时,其对应的NavBackStackEntry
和 ViewModel 才会被清除。
1.3.1 使用 Hilt
如果你使用的是Hilt,那么就很简单了。使用 hiltViewModel()
方法,它会自动找到当前作用域(即当前页面的 NavBackStackEntry
)的 ViewModel。
kt
// LoginViewModel.kt
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel // 关键注解
class LoginViewModel @Inject constructor(
// 你可以在这里注入其他依赖,如 Repository
private val userRepository: UserRepository
) : ViewModel() {
// ViewModel 的代码...
val uiState: MutableStateFlow<LoginUiState> = ...
fun login() { ... }
}
kt
// LoginScreen.kt
import androidx.hilt.navigation.compose.hiltViewModel // 关键导入
@Composable
fun LoginCompose(
navController: NavController,
// 如此声明,ViewModel 的作用域即是当前 composable 目的地
viewModel: LoginViewModel = hiltViewModel()
) {
// 使用 viewModel 中的状态和逻辑
val uiState by viewModel.uiState.collectAsState()
Button(onClick = { viewModel.login() }) {
Text("Login")
}
if (uiState.isSuccess) {
LaunchedEffect(Unit) {
navController.popBackStack() // 登录成功后返回
}
}
}
1.3.2:不使用 Hilt(手动管理)
添加依赖
kotlin
dependencies {
def lifecycle_version = "2.6.0" // 请使用最新稳定版
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}
kt
// LoginViewModel.kt
import androidx.lifecycle.ViewModel
class LoginViewModel(
private val userRepository: UserRepository // 依赖
) : ViewModel() {
// ... 同样的逻辑
}
kt
// LoginScreen.kt
import androidx.lifecycle.viewmodel.compose.viewModel // 关键导入
@Composable
fun LoginCompose(
navController: NavController
) {
// 这是最简洁的方式!
// viewModel() 函数会自动为你处理好一切
val loginViewModel: LoginViewModel = viewModel()
val uiState by loginViewModel.uiState.collectAsState()
// 使用 ViewModel
if (uiState.isSuccess) {
LaunchedEffect(Unit) {
navController.popBackStack()
}
}
// ... 其余 UI 代码
}
好的,这篇文章就介绍到这里~~