停止与 Compose 导航作斗争(这 5 个技巧将改变一切)

像专业人士一样驾驭多屏 Compose 应用,掌握 Google 文档不会教你的模式------这些模式是从真实的生产实践中学到的。

欢迎关注我的公众号:OpenFluter,感恩

🎉 恭喜,您已构建了第一个 Compose 应用。

一切都很美好,直到您想在屏幕之间传递数据 。那一刻,感觉就像在七月解开圣诞灯饰的缠绕,而导航 也让人一筹莫展。如果您曾盯着 NavHost 配置,心想"肯定有更好的办法",那么这篇文章就是您的救生圈。

现在,我将告诉您一些导航模式,它们让我对 Compose 开发的感觉从痛苦 变成了顺畅 。不是理论------而是来自交付实际应用的真实解决方案


忘掉基于字符串的路由 吧。它们容易出错,不利于重构,而且在大型代码库中极难追踪。密封类 (Sealed classes) 才是您导航的新骨干。

kotlin 复制代码
sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Profile : Screen("profile/{userId}") {
        fun createRoute(userId: String) = "profile/$userId"
    }
    object Settings : Screen("settings")
    
    // Complex arguments made simple
    object ProductDetail : Screen("product/{id}?source={source}") {
        fun createRoute(id: Int, source: String = "unknown") = 
            "product/$id?source=$source"
    }
}

这意味着:

路由在您的 IDE 中可以自动补全 。重构是安全的 。可以防止拼写错误破坏运行时的导航。通过消除一整类 Bug,这一个模式就能完成大量工作。

🚀 传递复杂对象,告别头疼

官方的建议?将所有内容序列化为字符串。现实情况呢?对于复杂对象来说,这令人痛苦 。没有人提及的秘诀是:使用一个作用域限定到导航图 (navigation graph) 的 ViewModel

kotlin 复制代码
// In your nav graph
composable(Screen.ProductList.route) {
    val parentEntry = remember(it) {
        navController.getBackStackEntry(Screen.Main.route)
    }
    val sharedViewModel: OrderViewModel = hiltViewModel(parentEntry)
    
    ProductListScreen(
        onProductClick = { product ->
            sharedViewModel.selectProduct(product)
            navController.navigate(Screen.ProductDetail.route)
        }
    )
}

💻 传递复杂对象与深度链接

🚀 传递复杂对象

您的 ProductDetail 屏幕可以共享同一个 ViewModel ------无需序列化,无需参数限制,也无需陷入 JSON 解析的困境。这种模式涵盖了屏幕之间的列表、复杂对象甚至 Lambda 表达式的传递。


🔗 深度链接的正确做法

生产环境中的问题: 深度链接(Deep Links)会因为开发者进行了错误的测试而失效。

诀窍是什么? 对深度链接进行"冷测试"(Cold Test) ------完全关闭您的应用程序,然后再点击该链接。

kotlin 复制代码
val navController = rememberNavController()

NavHost(
    navController = navController,
    startDestination = Screen.Home.route
) {
    composable(
        route = Screen.ProductDetail.route,
        deepLinks = listOf(
            navDeepLink {
                uriPattern = "myapp://product/{id}"
                action = Intent.ACTION_VIEW
            }
        )
    ) { backStackEntry ->
        val productId = backStackEntry.arguments?.getString("id")
        ProductDetailScreen(productId = productId)
    }
}

💡 深度链接与底部弹窗导航

📊 深度链接建议 (Tip: Deep Links)

提示: 为所有的深度链接 入口配置分析追踪(analytics)。最终,您就能知道哪些营销活动真正在驱动应用打开量,这些指标将影响您的开发路线图 (roadmap)。


模态底部弹窗 (Modal bottom sheets)也是一种目标页面(destination),但处理过程感觉很笨拙。下面是优雅的解决方案

kotlin 复制代码
// Define bottom sheet destinations in your sealed class
sealed class BottomSheet(val route: String) {
    object Filters : BottomSheet("filters_sheet")
    object Sort : BottomSheet("sort_sheet")
}

// In your scaffold
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)

ModalBottomSheetLayout(bottomSheetNavigator) {
    NavHost(navController, startDestination = Screen.Home.route) {
        bottomSheet(BottomSheet.Filters.route) {
            FiltersContent(
                onApply = { filters ->
                    // Apply filters
                    navController.popBackStack()
                }
            )
        }
    }
}

📲 底部弹窗与嵌套导航图

📉 底部弹窗技巧的意义

现在,这意味着您的底部弹窗 会自动参与导航状态返回键处理深度链接!无需进行手动状态管理。


🧩 嵌套导航图:要么做大,要么回家

多模块应用必须 使用嵌套导航图 ,这是必然的。当您的屏幕数量达到 15 到 20 个 时,扁平的导航结构就无法再维护了

kotlin 复制代码
fun NavGraphBuilder.authGraph(navController: NavController) {
    navigation(
        startDestination = Screen.Login.route,
        route = "auth_flow"
    ) {
        composable(Screen.Login.route) { LoginScreen() }
        composable(Screen.Signup.route) { SignupScreen() }
        composable(Screen.ForgotPassword.route) { ForgotPasswordScreen() }
    }
}

// In main NavHost
NavHost(navController, startDestination = "auth_flow") {
    authGraph(navController)
    homeGraph(navController)
    profileGraph(navController)
}

为什么? 每个功能模块 (feature module)拥有并管理自己的导航。团队可以独立工作测试隔离的 。不要污染您的主 NavHost


💫 感觉原生的动画过渡

默认的过渡效果非常突兀 (abrupt)。通过添加由用户期望驱动的自定义动画,为您的应用增添一丝优雅:

kotlin 复制代码
composable(
    route = Screen.ProductDetail.route,
    enterTransition = {
        slideIntoContainer(
            AnimatedContentTransitionScope.SlideDirection.Start,
            animationSpec = tween(300)
        )
    },
    exitTransition = {
        slideOutOfContainer(
            AnimatedContentTransitionScope.SlideDirection.Start,
            animationSpec = tween(300)
        )
    }
) { ProductDetailScreen() }

💾 大多数开发者错过的一件事

正确地保存和恢复状态。 您的应用必须能够在 Android 后台被杀掉后存活下来,包括其导航状态 。如果某个状态对于您的 UI 至关重要,请使用 rememberSaveable,并测试进程被杀死的情况。

数据为什么会丢失并不重要 ,因为您的用户只会觉得您的应用坏了,然后就会流失。状态恢复是区分专业应用和业余应用的试金石。


📋 您的导航清单

如果您掌握了这些模式,导航将是 Compose 中最简单的事情:

  • 使用密封类 (Sealed classes) 实现类型安全路由,杜绝基于字符串的 Bug。
  • 通过在导航图之间共享 ViewModel 来传递复杂对象。
  • 通过冷启动应用来测试深度链接,以捕获生产环境问题。
  • 使用底部弹窗导航器(bottom sheet navigator)来处理模态弹窗。
  • 使用基于功能的层级图(hierarchy level graphs)来实现可扩展的架构。
  • 添加符合导航方向且有意义的动画。
  • 能够从测试进程的"死亡"中恢复状态

导航并非总是那么顺心如意。每个应用都会有一些模型,但无论是一个小型应用还是更复杂的多模块架构,这些模式都将帮助您应对 95% 的实际用例 。从类型安全路由开始,然后随着应用的扩展逐渐采用其他模式。

您目前正在应对哪种导航难题?请留言------很可能我也经历过同样的挣扎,我很乐意分享有效的解决方案。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax