Android :Compose如何监听生命周期?NavHostController和我们传统的Activity的任务栈有什么不同?

先问几个问题:

在使用传统的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的操作。

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 来模拟和响应这种"进入/退出"组件树的行为。

  1. 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退出组合时,协程会自动取消
    }
  1. 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 // 页面不可见,停止计时器
            // 这里还可以做其他清理工作,比如取消网络请求、解除广播监听等
        }
    }

执行逻辑:

  1. 进入Main页时,DisposableEffect执行,打印日志并将 isTimerActive设为 trueLaunchedEffect检测到 isTimerActive变为 true,启动计时协程。
  2. 跳转到 LoginMainScaffold从 Composable 树中被​移除​DisposableEffectonDispose块被调用,打印日志并将 isTimerActive设为 falseLaunchedEffect的协程因为其 key(isTimerActive) 变化或 Composable 退出,会​自动取消​
  3. 返回到main页,MainScaffold被重新组合到 Composable 树中。整个过程从第 1 步开始​重复​

总结一下就是:

概念 Activity 生命周期 Compose 方式(等效实现)
​页面创建/进入​ onCreate()-> onStart()-> onResume() DisposableEffect { ...; onDispose{} }内部代码 + LaunchedEffect
​页面退出/不可见​ onPause()-> onStop() DisposableEffectonDispose
​页面被系统销毁​ onDestroy() onDispose块(但 Compose 导航默认保持后台栈,通常不会销毁)

那么我想知道,这些页面的viewmodel又是如何使用的?

1.3 compose中使用viewmodel,他的生命周期如何,作用域如何

首先要理解一个关键点:​​导航库将每个目的地(composable)与一个 NavBackStackEntry关联起来​ ​。这个 NavBackStackEntry就是 ViewModel 的​​作用域单位​​。

  • ​默认情况(不共享)​ ​:当你导航到一个新页面时,会创建一个新的 NavBackStackEntry。在此作用域内获取的 ViewModel 是​​全新的、独立的​ ​,与其他页面的 ViewModel ​​不共享​​。

  • ​返回时​ ​:当你从新页面返回,该页面对应的 NavBackStackEntry及其 ViewModel 会被保留在后台栈中(直到被弹出)。因此,返回时页面状态(如输入框文本、列表滚动位置)得以保留,正是得益于 ViewModel 的存活。

  • ​永久离开时​ ​:当你通过 popBackStacknavigate(...) { 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 代码
}

好的,这篇文章就介绍到这里~~

相关推荐
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android