Jetpack Compose 多页面架构实战:从 Splash 到底部导航,每个 Tab 拥有独立 ViewModel

在构建现代 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 应用。

记住:好的架构不是一开始就完美,而是在演进中保持清晰。

代码后续补充...

相关推荐
ab_dg_dp5 小时前
Android bugreportz 源码分析
android
湘-枫叶情缘5 小时前
BIZBOK®指南入门教案:框架全景理解与知识地图构建
架构
云舟吖5 小时前
Open-AutoGLM 部署指南:本地大模型控制 Android 手机自动化
架构
踏浪无痕6 小时前
自定义 ClassLoader 动态加载:不重启就能加载新代码?
后端·面试·架构
踏浪无痕6 小时前
别重蹈我们的覆辙:脚本引擎选错的两年代价
后端·面试·架构
木风小助理6 小时前
如何破解 MySQL 死锁?核心原则与实操方法
android
小吴学不废Java6 小时前
MySQL慢查询日志分析
android·adb
狂胤6 小时前
告别“CV工程师”:手把手教你设计一套 B 端低代码 DSL
架构
喷火龙8号6 小时前
JWT 认证方案深度对比:单 Token 扩展刷新 vs 双 Token 验证
后端·设计模式·架构