停止与 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% 的实际用例 。从类型安全路由开始,然后随着应用的扩展逐渐采用其他模式。

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

相关推荐
司铭鸿14 分钟前
图论中的协同寻径:如何找到最小带权子图实现双源共达?
linux·前端·数据结构·数据库·算法·图论
风宇啸天36 分钟前
令牌桶按用户维度限流
前端
safestar201238 分钟前
React 19 深度解析:从并发模式到数据获取的架构革命
前端·javascript·react.js
越努力越幸运5081 小时前
npm常见问题解决
前端·npm·node.js
Wild~~1 小时前
electron-vite
前端·javascript·electron
by__csdn1 小时前
Electron+Vite:实现electron + vue3 + ts + pinia + vite高效跨平台开发指南
前端·javascript·vue.js·typescript·electron·node.js·vue
马达加斯加D1 小时前
C# --- 如何写UT
前端·c#·log4j
yqcoder1 小时前
在 scss 中,&>div 作用
前端·css·scss
小马哥编程2 小时前
这个variables.scss文件中$menuText:#bfcbd9;:export {menuText: $menuText; }的语法符合要求吗
前端·css·scss
宋辰月2 小时前
zustand
前端·javascript·html