在构建现代 Android 应用时,清晰的页面结构和合理的状态管理 是保证项目可维护性和可扩展性的关键。Jetpack Compose 提供了声明式 UI 的强大能力,而 Navigation + ViewModel 的组合,则是实现复杂多页面应用的黄金搭档。
本文将通过一个 完整、可运行、生产级风格 的案例,带你一步步实现:
- 启动页(Splash)
- 登录页(Login)
- 主页(带底部导航:首页 / 通讯录 / 我的)
- 每个 Tab 拥有自己独立的 ViewModel
- 全局登录状态统一管理
并深入探讨 页面拆分、导航设计、状态隔离 等核心工程实践。
🎯 最终效果预览

所有页面逻辑分离,职责清晰,切换 Tab 不会丢失状态!
📁 推荐项目结构
良好的目录结构是大型项目的基石:
groovy
com.example.myapp/
├── MyApp.kt // App 根组件
├── navigation/NavGraph.kt // 全局导航图
├── viewmodel/AuthViewModel.kt // 全局登录状态
├── route/ModuleRoute.kt // 路由常量
└── ui/
├── splash/SplashScreen.kt
├── login/LoginScreen.kt
└── main/
├── MainScreen.kt // 底部导航容器
├── home/HomeTab.kt + HomeViewModel.kt
├── contacts/ContactsTab.kt + ContactsViewModel.kt
└── profile/ProfileTab.kt + ProfileViewModel.kt
🔑 核心依赖
json
implementation ("androidx.navigation:navigation-compose:2.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
implementation ("androidx.lifecycle:lifecycle-runtime-compose:2.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0")
1️⃣ 全局状态:AuthViewModel
用于管理用户是否已登录,供整个 App 使用:
kotlin
class AuthViewModel : ViewModel() {
private val _flowLogin = MutableStateFlow(false)
val flowLogin = _flowLogin.asStateFlow()
// 模拟登录成功
fun login() {
_flowLogin.value = true
}
fun loginOut() {
_flowLogin.value = false
}
}
在 MyApp.kt 中通过 viewModel() 获取单例实例。
2️⃣ 页面专属 ViewModel:解耦业务逻辑
通讯录 ViewModel
kotlin
data class Contact(val id: Int, val name: String, val phone: String)
class ContactsViewModel : ViewModel() {
private val _contacts = MutableStateFlow<MutableList<Contact>>(mutableListOf())
val contacts: StateFlow<List<Contact>> = _contacts.asStateFlow()
private var _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow()
init {
loadContacts()
}
private fun loadContacts() {
viewModelScope.launch {
_isLoading.value = true
delay(1000) // 模拟网络请求
_contacts.value = mutableListOf(
Contact(1, "张三", "138****1234"),
Contact(2, "李四", "139****5678")
)
_isLoading.value = false
}
}
fun refresh() {
loadContacts()
}
}
💡 优势:
- 数据加载、错误处理、刷新逻辑全部封装在 ViewModel
- UI 层只负责展示,完全无业务逻辑
- 切换 Tab 时,ViewModel 实例由 Navigation 自动保存(只要 route 不变)
3️⃣ 页面 UI:自动注入 ViewModel
在 Composable 中直接使用 viewModel() 获取专属实例:
kotlin
@Composable
fun ContactsTab(
contactsViewModel: ContactsViewModel = viewModel(),
modifier: Modifier = Modifier,
) {
val contacts by contactsViewModel.contacts.collectAsStateWithLifecycle()
val isLoading by contactsViewModel.isLoading.collectAsStateWithLifecycle()
Box(
modifier = modifier.fillMaxSize(), contentAlignment = Alignment.TopStart
) {
Text("📇 通讯录", fontSize = 24.sp)
}
if (isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(top = 56.dp),
contentPadding = PaddingValues(16.dp)
) {
items(contacts.size) { contact ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
contacts[contact].name,
style = MaterialTheme.typography.titleMedium
)
Text(
contacts[contact].phone,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}
}
✅ collectAsStateWithLifecycle() 会自动在 Composable 进入后台时暂停收集,避免内存泄漏。
4️⃣ 导航设计:嵌套路由 + 状态清理
全局导航图(NavGraph.kt)
kotlin
@Composable
fun NavGraph(navigationControl: NavHostController, authViewModel: AuthViewModel) {
NavHost(navController = navigationControl, startDestination = ModuleRoute.Splash) {
composable<ModuleRoute.Splash> {
SplashScreen(onTimeOut = {
if (authViewModel.flowLogin.value) {
navigationControl.navigate(ModuleRoute.Main) {
popUpTo(ModuleRoute.Splash) { inclusive = true }
}
} else {
navigationControl.navigate(ModuleRoute.Login) {
popUpTo(ModuleRoute.Splash) { inclusive = true }
}
}
})
}
composable<ModuleRoute.Login> {
LoginScreen(onLoginClick = {
authViewModel.login()
navigationControl.navigate(ModuleRoute.Main) {
popUpTo(ModuleRoute.Login) { inclusive = true }
}
})
}
composable<ModuleRoute.Main> {
MainScreen(onLogout = {
authViewModel.loginOut()
navigationControl.navigate(ModuleRoute.Login) {
popUpTo(ModuleRoute.Main) { inclusive = true }
}
})
}
}
}
主页内部:嵌套 NavHost 实现底部导航
kotlin
@Composable
fun MainScreen(
onLogout: () -> Unit,
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
) {
// 定义底部tab
val items = listOf(
BottomNavItem.Home,
BottomNavItem.Contacts,
BottomNavItem.Profile
)
BackHandler(enabled = true){
Log.e("test","进入BackHandler")
}
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = null) },
label = { Text(item.title) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
// 避免重复入栈
popUpTo(navController.graph.id) {
saveState = true
inclusive = false
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
},
) { innerPadding ->
NavHost(
navController = navController,
startDestination = BottomNavItem.Home.route,
modifier = modifier.padding(innerPadding)
) {
composable(BottomNavItem.Home.route) {
HomeTab()
}
composable(BottomNavItem.Contacts.route) {
ContactsTab()
}
composable(BottomNavItem.Profile.route) {
ProfileTab(onLogout = onLogout)
}
}
}
}
// 定义底部导航项
sealed class BottomNavItem(val route: String, val title: String, val icon: ImageVector) {
data object Home : BottomNavItem("home", "首页", Icons.Default.Home)
data object Contacts : BottomNavItem("contacts", "通讯录", Icons.Default.Person)
data object Profile : BottomNavItem("profile", "我的", Icons.Default.AccountCircle)
}
🌟 为什么用嵌套路由?
官方推荐做法!避免底部 Tab 切换时重建整个页面,同时支持每个 Tab 内部继续跳转子页面(如联系人详情)。
5️⃣ 关键技巧总结
| 场景 | 解决方案 |
|---|---|
| 页面太多? | 拆分到不同文件,按功能模块组织目录 |
| 状态混乱? | 全局状态用共享 ViewModel,局部状态用页面专属 ViewModel |
| 返回栈错乱? | 使用 popUpTo(...) + inclusive = true 清理历史 |
| Tab 切换重建? | 确保使用嵌套路由,Navigation 会自动保存状态 |
| 内存泄漏? | 用 collectAsStateWithLifecycle() 替代 collectAsState() |
✅ 为什么这样做是"最佳实践"?
- 高内聚低耦合: 每个页面只关心自己的数据和 UI
- 易于测试: ViewModel 可单元测试,UI 可 Preview 预览
- 团队协作友好: 多人开发不同 Tab 互不干扰
- 可扩展性强: 未来添加"消息"Tab 只需复制模式
- 符合官方架构指南: 遵循 Guide to app architecture
📚 结语
Jetpack Compose 不只是"新 UI 框架",更是一种 全新的应用架构思维。通过合理拆分页面、隔离状态、规范导航,我们可以构建出既简洁又强大的现代化 Android 应用。
记住:好的架构不是一开始就完美,而是在演进中保持清晰。
代码后续补充...