# Compose Navigation -- Reference Patterns

Full code for the patterns summarized in `SKILL.md`. Load the section you need
when implementing that specific navigation feature.

## Contents

- [Custom NavType Type-Map](#custom-navtype-type-map)
- [SafeNavController](#safenavcontroller)
- [Navigation as a One-Shot Effect](#navigation-as-a-one-shot-effect)

## Custom NavType Type-Map

Type-safe routes serialize primitives out of the box. A complex `@Serializable`
argument needs a custom `NavType` plus a `typeMap` shared by the
`composable<>()` registration and every `toRoute` read. Without the type-map,
only primitive nav args resolve and the route fails to build.

```kotlin
@Serializable
data class SearchFilter(val category: String, val minPrice: Int, val maxPrice: Int)

@Serializable
data class SearchRoute(val filter: SearchFilter)

val SearchFilterNavType = object : NavType<SearchFilter>(isNullableAllowed = false) {
    override fun get(bundle: Bundle, key: String): SearchFilter? =
        bundle.getString(key)?.let { Json.decodeFromString(it) }

    override fun parseValue(value: String): SearchFilter =
        Json.decodeFromString(Uri.decode(value))

    override fun serializeAsValue(value: SearchFilter): String =
        Uri.encode(Json.encodeToString(value))

    override fun put(bundle: Bundle, key: String, value: SearchFilter) {
        bundle.putString(key, Json.encodeToString(value))
    }
}
```

### Shared holder

One holder owns the map so the registration and every read agree:

```kotlin
object NavTypeMaps {
    val search: Map<KType, NavType<*>> =
        mapOf(typeOf<SearchFilter>() to SearchFilterNavType)
}
```

### Registration and read

```kotlin
composable<SearchRoute>(typeMap = NavTypeMaps.search) { entry ->
    val route = entry.toRoute<SearchRoute>(typeMap = NavTypeMaps.search)
    SearchScreen(filter = route.filter)
}
```

### Reading in the ViewModel

`SavedStateHandle.toRoute` takes the same map:

```kotlin
@HiltViewModel
class SearchViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val filter: SearchFilter =
        savedStateHandle.toRoute<SearchRoute>(typeMap = NavTypeMaps.search).filter
}
```

## SafeNavController

Feature code should never hold a raw `NavHostController`. Wrap it so features see
only the methods they need, and wrap each call in `runCatching` so a
double-navigation or illegal-state exception from a rapid double tap is
swallowed instead of crashing.

```kotlin
class SafeNavController(private val controller: NavHostController) {

    fun navigate(route: Any) {
        runCatching { controller.navigate(route) }
    }

    fun navigateReplacing(route: Any, popUpToType: KClass<*>) {
        runCatching {
            controller.navigate(route) {
                popUpTo(popUpToType) { inclusive = true }
                launchSingleTop = true
            }
        }
    }

    fun navigateClearingStack(route: Any) {
        runCatching {
            controller.navigate(route) { popUpTo(0) { inclusive = true } }
        }
    }

    fun back() {
        runCatching { controller.popBackStack() }
    }
}

val LocalSafeNavController = staticCompositionLocalOf<SafeNavController> {
    error("SafeNavController not provided")
}
```

### Providing it once at the NavHost

```kotlin
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    val safeNav = remember(navController) { SafeNavController(navController) }
    CompositionLocalProvider(LocalSafeNavController provides safeNav) {
        NavHost(navController = navController, startDestination = HomeRoute) {
            composable<HomeRoute> { HomeRoute() }
            composable<DetailRoute> { DetailRoute() }
        }
    }
}
```

Feature screens read `LocalSafeNavController.current` and never receive the raw
controller as a parameter.

## Navigation as a One-Shot Effect

The ViewModel emits navigation intents as one-shot events; the composable that
owns the `NavController` translates each into a navigation call. This keeps the
ViewModel free of navigation types and unit-testable (assert emitted events, no
`NavController` mock needed).

### ViewModel side

```kotlin
sealed interface HomeNavEvent {
    data class ToDetail(val id: String) : HomeNavEvent
    data object ToSearch : HomeNavEvent
}

@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel() {

    private val _navEvents = Channel<HomeNavEvent>(Channel.BUFFERED)
    val navEvents: Flow<HomeNavEvent> = _navEvents.receiveAsFlow()

    fun onItemClick(id: String) {
        viewModelScope.launch { _navEvents.send(HomeNavEvent.ToDetail(id)) }
    }

    fun onSearchClick() {
        viewModelScope.launch { _navEvents.send(HomeNavEvent.ToSearch) }
    }
}
```

### Composable side

```kotlin
@Composable
fun HomeRoute(viewModel: HomeViewModel = hiltViewModel()) {
    val nav = LocalSafeNavController.current
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) {
        viewModel.navEvents.collect { event ->
            when (event) {
                is HomeNavEvent.ToDetail -> nav.navigate(DetailRoute(event.id))
                HomeNavEvent.ToSearch -> nav.navigate(SearchRoute(SearchFilter("BOOKS", 0, 50)))
            }
        }
    }

    HomeScreen(
        state = uiState,
        onItemClick = viewModel::onItemClick,
        onSearchClick = viewModel::onSearchClick,
    )
}
```

The `Channel` (not a `StateFlow`) guarantees each navigation fires once and is
not replayed on recomposition or config change. This complements the event seam
in the `android-mvi-viewmodel` skill.
