Files
flyLink/skylink_android_app/ADVANCED_NAVIGATION.md
flowerstonezl 056289625c init
2026-01-19 17:08:49 +08:00

11 KiB
Raw Blame History

🗺️ Navigation 高级用法指南

📌 Overview

本文档展示如何使用高级的 Navigation 扩展和工具类来优雅地管理应用导航。


🎯 核心概念

1. Routes 常量对象

object Routes {
    const val HOME = "home"
    const val SETTINGS = "settings"
}

2. NavigationEvent 密封类

sealed class NavigationEvent {
    object NavigateToHome : NavigationEvent()
    object NavigateToSettings : NavigationEvent()
    object NavigateUp : NavigationEvent()
    data class NavigateToRoute(val route: String) : NavigationEvent()
}

3. NavigationManager 工具类

提供统一的导航操作接口,包括:

  • 安全导航 (防止重复点击)
  • 带 Back Stack 清理的导航
  • 事件处理

💻 代码示例

基础导航

在 Activity 中:

class MainActivity : AppCompatActivity() {
    private lateinit var navController: NavController
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        setupNavigation()
    }
    
    private fun setupNavigation() {
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController
    }
}

快速导航方法

方式 1使用 NavController 扩展

// 快速导航到首页
navController.navigateToHome()

// 快速导航到设置
navController.navigateToSettings()

方式 2使用 NavigationManager

// 安全导航
NavigationManager.navigateSafe(navController, Routes.HOME)

// 导航并清理 Back Stack
NavigationManager.navigateWithClear(
    navController, 
    Routes.SETTINGS,
    popUpTo = Routes.HOME,
    inclusive = false
)

方式 3处理导航事件

val event = NavigationEvent.NavigateToSettings
NavigationManager.handleNavigationEvent(navController, event)

在 Fragment 中使用

SettingsFragment 中导航:

class SettingsFragment : Fragment() {
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        val navController = findNavController()
        
        // 导航回首页
        someButton.setOnClickListener {
            navController.navigateToHome()
        }
    }
}

在 ViewModel 中使用

使用 LiveData 和导航事件:

class AppViewModel : ViewModel() {
    private val _navigationEvent = MutableLiveData<NavigationEvent>()
    val navigationEvent: LiveData<NavigationEvent> = _navigationEvent
    
    fun goToSettings() {
        _navigationEvent.value = NavigationEvent.NavigateToSettings
    }
    
    fun goBack() {
        _navigationEvent.value = NavigationEvent.NavigateUp
    }
}

在 Fragment 中观察:

class HomeFragment : Fragment() {
    private val viewModel: AppViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewModel.navigationEvent.observe(viewLifecycleOwner) { event ->
            val navController = findNavController()
            NavigationManager.handleNavigationEvent(navController, event)
        }
    }
}

🛡️ 防止重复导航

问题描述

快速点击按钮可能导致多次导航,造成 back stack 混乱。

解决方案

使用 NavigationManager.navigateSafe():

// ❌ 不安全 - 可能重复导航
button.setOnClickListener {
    navController.navigate(R.id.settingsFragment)
}

// ✅ 安全 - 检查当前目的地
button.setOnClickListener {
    navController.navigateSafe(Routes.SETTINGS)
}

原理:

fun navigateSafe(navController: NavController, route: String) {
    try {
        if (navController.currentDestination?.route != route) {
            navController.navigate(route) {
                launchSingleTop = true  // 防止堆栈多个实例
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

🔄 Back Stack 管理

清理 Back Stack

场景:用户登录后不应返回到登录页

// 导航到首页,并清理 Back Stack
navController.navigateAndClear(
    route = Routes.HOME,
    popUpTo = Routes.SETTINGS  // 清理此位置及以下
)

// 等价于
NavigationManager.navigateWithClear(
    navController,
    Routes.HOME,
    popUpTo = Routes.SETTINGS,
    inclusive = true
)

完整示例

// 登录成功后导航到首页,并清理登录页
private fun handleLoginSuccess() {
    navController.navigate(Routes.HOME) {
        // 清理所有登录相关的 Fragment
        popUpTo(Routes.LOGIN) {
            inclusive = true  // 包含登录页本身
        }
        launchSingleTop = true
    }
}

🎯 NavigationHelper 使用

在 MainActivity 中初始化:

class MainActivity : AppCompatActivity() {
    private lateinit var navController: NavController
    private lateinit var navigationHelper: NavigationHelper
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController
        navigationHelper = NavigationHelper(navController)
    }
    
    override fun onBackPressed() {
        if (!navigationHelper.handleBackPressed()) {
            @Suppress("DEPRECATION")
            super.onBackPressed()
        }
    }
}

功能方法:

// 获取当前路由
val currentRoute = navigationHelper.getCurrentRoute()

// 检查是否在特定页面
if (navigationHelper.isAtTopDestination(Routes.HOME)) {
    // 在首页
}

// 处理后退
navigationHelper.handleBackPressed()

📊 导航流程图

┌─────────────────────┐
│   NavigationEvent   │
│  (密封类定义事件)    │
└──────────┬──────────┘
           │
    ┌──────▼──────┐
    │ NavigationExt │
    │ (扩展方法)    │
    └──────┬──────┘
           │
    ┌──────▼──────────────┐
    │ NavigationManager   │
    │ (工具类处理逻辑)    │
    └──────┬──────────────┘
           │
    ┌──────▼──────────────┐
    │  NavController      │
    │ (执行导航)          │
    └─────────────────────┘

🔀 高级导航场景

场景 1带参数的导航

// 定义路由
const val DETAIL = "detail/{id}"
const val DETAIL_ROUTE = "detail"

// 导航时传递参数
navController.navigate("$DETAIL_ROUTE/123")

// 在 DetailFragment 中接收
val id = arguments?.getString("id")

场景 2条件导航

fun navigateBasedOnUserState(user: User?) {
    when {
        user == null -> navController.navigateSafe(Routes.LOGIN)
        user.isProfileComplete -> navController.navigateSafe(Routes.HOME)
        else -> navController.navigateSafe(Routes.SETUP_PROFILE)
    }
}

场景 3链式导航

// 多步导航
fun performLoginFlow() {
    // 第 1 步:导航到验证码页
    navController.navigate(Routes.VERIFICATION)
    
    // 验证成功后,导航到主页
    // (通过 ViewModel 事件驱动)
}

⚙️ 扩展 NavigationEvent

添加新的导航事件

sealed class NavigationEvent {
    // 现有事件...
    object NavigateToHome : NavigationEvent()
    object NavigateToSettings : NavigationEvent()
    
    // 新增事件
    data class NavigateWithData(
        val route: String,
        val data: Bundle
    ) : NavigationEvent()
    
    data class NavigateToDetail(val id: String) : NavigationEvent()
}

处理新事件

fun NavigationManager.handleNavigationEvent(navController: NavController, event: NavigationEvent) {
    when (event) {
        // 现有处理...
        is NavigationEvent.NavigateWithData -> {
            navController.navigate(event.route, event.data)
        }
        is NavigationEvent.NavigateToDetail -> {
            val route = "${Routes.DETAIL_ROUTE}/${event.id}"
            navigateSafe(navController, route)
        }
    }
}

🧪 测试导航

单元测试

@RunWith(RobolectricTestRunner::class)
class NavigationManagerTest {
    
    private lateinit var navController: NavController
    
    @Before
    fun setUp() {
        navController = mockk<NavController>()
    }
    
    @Test
    fun testNavigateSafe() {
        NavigationManager.navigateSafe(navController, Routes.HOME)
        
        verify {
            navController.navigate(Routes.HOME, any())
        }
    }
}

集成测试

@RunWith(AndroidJUnit4::class)
class NavigationIntegrationTest {
    
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)
    
    @Test
    fun testNavigationToSettings() {
        onView(withId(R.id.settings_button)).perform(click())
        
        onView(withId(R.id.settings_fragment)).check(
            matches(isDisplayed())
        )
    }
}

📚 最佳实践

推荐做法

  1. 使用 NavigationManager - 集中管理导航逻辑
  2. 创建扩展方法 - 为常见操作创建快捷方式
  3. 使用 ViewModel - 将导航事件与业务逻辑分离
  4. 安全导航 - 始终使用 navigateSafe()
  5. 清理 Back Stack - 适当时清理堆栈

避免做法

  1. 直接调用 navigate() - 容易出现重复导航
  2. 硬编码路由 - 使用常量对象
  3. 混合导航方式 - 统一使用扩展或工具类
  4. 忘记处理异常 - 始终 try-catch
  5. 不清理 Back Stack - 导致内存泄漏

📞 快速参考

操作 方法
导航到首页 navController.navigateToHome()
导航到设置 navController.navigateToSettings()
安全导航 navController.navigateSafe(route)
清理导航 navController.navigateAndClear(route)
处理事件 NavigationManager.handleNavigationEvent()
后退 navController.navigateUp()
获取当前路由 navigationHelper.getCurrentRoute()

📖 相关文档


完成日期: 2026-01-19
版本: 1.0
状态: 完成