目录:
- 前言
- 定义导航图(Navigation Graph)和路由(Route),用代码声明页面跳转逻辑 2.1 composeable页面卸载的时候,数据如何保存? 2.2 composeable挂载页面的时候,是覆盖,还是替换呢?
一、前言
最近在学习compose,先是找文章来学习,然后看github上面的源码来学习,最后再找视频来学习,然后再上github的项目学习,接着在根据项目的源码,自己删除再重新写一遍。一波三折。
在学习的过程,我就有一个疑问,只有一个Activity,也没有看到Fragment呢,也没有看到更多的Fragment,我好奇,compose的生命周期是如何的,他的页面又是如何加载的,没看到onstart方法,或者页面停止的时候onstop方法。
接下来,我们来解答一下这些疑惑。
二、通过定义导航图(Navigation Graph)和路由(Route),用代码声明页面跳转逻辑
在 Jetpack Compose 中,不再推荐使用多个 Activity 或 Fragment 来实现页面跳转,而是采用 单一 Activity + 多 Composable 组件 + 导航库 的架构。这是 Compose 的核心理念:通过声明式 UI 和状态驱动的导航来简化开发。
- 单一 Activity:整个应用通常只有一个 Activity(MainActivity),所有页面切换通过 Composable 组件 的替换完成。
- 无 Fragment:Compose 完全抛弃 Fragment,直接通过 NavController 和 NavHost 管理页面栈。
- 声明式导航:通过定义导航图(Navigation Graph)和路由(Route),用代码声明页面跳转逻辑。
那么,为什么要使用这种方式呢?
- 传统多 Activity/Fragment 的痛点: 1.1 每个 Activity 都有自己的生命周期,跨页面数据传递需要序列化(Parcelable/Serializable),容易导致内存泄漏或状态不一致 。 1.2 每次跳转新 Activity 都会触发冷启动,导致界面卡顿。
- Compose 新架构的核心优势 2.1 Composable 函数根据状态自动重组,无需手动更新视图。页面切换本质是 Composable 组件的重组,无需启动新 Activity 或 Fragment。 2.2 通过 NavController 管理全局导航栈,跳转逻辑集中且类型安全。
接下来,我们就看看导航是如何使用的(需要有一定基础~~)。
2.1 代码例子
1、导航代码
kotlin
fun AppScaffold() {
val navCtrl = rememberNavController() // 获取导航控制器
val navBackStackEntry by navCtrl.currentBackStackEntryAsState() // 监听导航栈变化 从而确定当前选中的底部导航项。这样可以确保底部导航栏的状态与当前显示的路由一致。
val currentDestination = navBackStackEntry?.destination // 当前路由目标
//接下来我要构建底部一个导航栏,所以需要使用到Scaffold
Scaffold(
modifier = Modifier.
statusBarsPadding().
navigationBarsPadding(),
//底部的导航栏内容,会放到NavHost里面。
bottomBar = {
when (currentDestination?.route) {
RouteName.HOME-> {BottomNavBarView(navCtrl = navCtrl)}
RouteName.CATEGORY-> {BottomNavBarView(navCtrl = navCtrl)}
RouteName.COLLECTION-> {BottomNavBarView(navCtrl = navCtrl)}
RouteName.PROFILE-> {BottomNavBarView(navCtrl = navCtrl)}
}
},
content = {
// 定义需要保持状态的页面索引(防止页面切换时重置)
var homeIndex = remember { 0 }
var categoryIndex = remember { 0 }
// 导航宿主容器
NavHost(
modifier = Modifier.background(MaterialTheme.colors.background),
navController = navCtrl,
startDestination = RouteName.HOME // 初始路由
) {
// 首页路由
composable(route = RouteName.HOME) {
HomePage(navCtrl)
}
// 分类页路由
composable(route = RouteName.CATEGORY) {
CategoryPage(navCtrl)
}
// 收藏页路由
composable(route = RouteName.COLLECTION) {
CollectPage(navCtrl)
}
// 个人中心页路由
composable(route = RouteName.PROFILE) {
ProfilePage(navCtrl)
}
}
}
)
}
2、点击切换
kotlin
@Composable
fun BottomNavBarView(navCtrl: NavHostController) {
//导航的标题,图片,路径
val bottomNavList = listOf(
BottomNavRoute.Home,
BottomNavRoute.Category,
BottomNavRoute.Collection,
BottomNavRoute.Profile
)
//底部导航栏ui。
BottomNavigation {
val navBackStackEntry by navCtrl.currentBackStackEntryAsState() // 监听导航栈变化 从而确定当前选中的底部导航项。这样可以确保底部导航栏的状态与当前显示的路由一致。
val currentDestination = navBackStackEntry?.destination// 当前路由目标
bottomNavList.forEach { screen ->
BottomNavigationItem(
modifier = Modifier.background(AppTheme.colors.themeUi),
icon = {
Icon(
imageVector = screen.icon,
contentDescription = null
)
},
label = { Text(text = stringResource(screen.stringId)) },
//判断当前页面是否在某个导航图中
selected = currentDestination?.hierarchy?.any { it.route == screen.routeName } == true,//那个被选择,那个就高亮。
onClick = {
//如果点击相同的,那么就不管
if (currentDestination?.route == screen.routeName) {
return@BottomNavigationItem
}
//跳转
navCtrl.navigate(screen.routeName)
}
)
}
}
}
3、HomePage :一个背景,其他页面也都是,用来测试,就写的简单一些~
kotlin
@Composable
fun HomePage(
navCtrl: NavHostController,
) {
Box(
modifier =Modifier.fillMaxSize().background(Color.Red)
)
}
这里,还有一个疑问,那么他重组,其他页面内容又是如何保存的呢?还是说直接就销毁了呢?比如从首页切换到分类,那么是覆盖,还是什么呢?
2.2 挂载和卸载
Composable 的挂载/卸载:当 Composable 进入/退出组合(Composition)时,可以视为类似 onStart/onStop 的时机。可以通过
1、挂载(进入组合)
当 Composable 首次被调用 或 在重组中被重新需要 时,会挂载到组合中。
初始化状态:通过 remember 或 mutableStateOf 创建的状态会被初始化。 执行副作用:如 LaunchedEffect、DisposableEffect 的首次运行。 构建 UI 树:将 Composable 添加到 Compose 的 UI 树中。
示例
kotlin
@Composable
fun MyComponent() {
// 挂载时初始化状态
val count = remember { mutableStateOf(0) }
// 挂载时启动副作用
LaunchedEffect(Unit) {
delay(1000)
println("Component mounted!")
}
Text("Count: ${count.value}")
}
2、卸载(退出组合)
当 Composable 在重组过程中不再被调用 时,会从组合中卸载。
常见场景:
条件语句跳过:因 if/when 条件不满足而不再执行。 导航离开页面:通过 NavController 跳转到其他页面。 父组件卸载:父 Composable 被卸载导致子组件连带卸载。
行为: 清理副作用:触发 DisposableEffect 的 onDispose 或取消 LaunchedEffect 的协程。 释放资源:如取消网络请求、关闭数据库连接。 从 UI 树移除:Composable 的 UI 节点被销毁。
示例
kotlin
@Composable
fun ParentComponent(showChild: Boolean) {
if (showChild) {
ChildComponent() // 挂载
}
// 当 showChild 变为 false 时,ChildComponent 卸载,因为条件不满足了,这里要好好理解。因为ChildComponent方法不被调用了,就会卸载掉。
}
@Composable
fun ChildComponent() {
// 副作用:挂载时启动,卸载时清理
DisposableEffect(Unit) {
val resource = allocateResource()
onDispose {
releaseResource(resource) // 卸载时执行清理
}
}
Text("Child Component")
}
现在我们可以回答一个问题,比如从首页切换到分类,那么是覆盖,还是什么呢?不是覆盖,是重新绘制,消失就会被卸载掉,不会留着,绘制变更的内容。
那么页面数据呢?也会初始化,重新获取数据?看你使用的是什么样的。
一、Composable 卸载后的状态
-
状态是否销毁?
普通状态(remember): 使用 remember 保存的状态 会被销毁。当 Composable 卸载后,其内部状态会被释放,下次挂载时会重新初始化。
kotlin@Composable fun Counter() { val count = remember { mutableStateOf(0) } // 卸载时销毁 Button(onClick = { count.value++ }) { Text("Count: ${count.value}") } }
可保存状态(rememberSaveable): 使用 rememberSaveable 的状态会在 配置变更(如屏幕旋转)时保留,但 Composable 完全卸载后依然会被销毁。
kotlin
val count = rememberSaveable { mutableStateOf(0) } // 配置变更保留,卸载销毁
-
副作用是否清理?
LaunchedEffect/DisposableEffect: 卸载时会自动取消协程或触发 onDispose 清理资源。
kotlinDisposableEffect(Unit) { // 挂载时执行 onDispose { // 卸载时清理 } }
二、重新挂载时的行为
当 Composable 再次被调用进入组合 时(例如条件语句重新满足、导航返回页面等),会触发 重新挂载,此时:
- 重新执行整个函数体
Composable 函数会从头到尾重新执行,包括:
所有 remember 的初始化
所有副作用的启动(如 LaunchedEffect)
- 状态重置
除非使用 状态提升 或 持久化存储,否则状态会被重置:
kotlin
// 示例:重新挂载时状态重置
@Composable
fun DynamicComponent(visible: Boolean) {
if (visible) {
val count = remember { mutableStateOf(0) } // 每次挂载都从 0 开始
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
}
那么怎么办呢?使用viewmodel,大家应该都知道viewmodel的生命周期。其生命周期与 Activity/Fragment 或导航图绑定。通过 ViewModel 保存状态。
kotlin
@Composable
fun PersistentCounter(viewModel: CounterViewModel = viewModel()) {
Button(onClick = { viewModel.increment() }) {
Text("Count: ${viewModel.count.value}")
}
}
class CounterViewModel : ViewModel() {
val count = mutableStateOf(0)
fun increment() { count.value++ }
}
比如说,我们发起一起请求,请求到的数据放到viewModel中,那么重新挂载的时候,直接把数据拿出来就可以,不需要重新执行请求。