Best practices for Type-Safe Navigation (Nav 3) and Adaptive Layouts in Upnext.
This skill encapsulates the critical patterns for implementing adaptive layouts and navigation in the Upnext application. It specifically addresses "Gotchas" related to ListDetailPaneScaffold and Compose Navigation 3.
Upnext uses NavigableListDetailPaneScaffold to support phones, tablets, and foldables.
[!NOTE] Adaptive UI utilities, such as
WindowSizeClassUtiland the@ReferenceDevicesPreview annotation, are now centralized in the:core:designsystemmodule to prevent circular dependencies. Ensure feature modules depend on:core:designsystemto use them.
NEVER use rememberSupportingPaneScaffoldNavigator with NavigableListDetailPaneScaffold. This causes back navigation conflicts and UI flickering.
✅ Correct Usage:
// MainScreen.kt
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator<Any>()
NavigableListDetailPaneScaffold(
navigator = listDetailNavigator,
defaultBackBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange, // Let scaffold handle pane back
// ...
)
The scaffold handles back navigation between panes (Detail -> List) automatically if defaultBackBehavior is set correctly.
BackHandler if the scaffold can handle it.BackHandler only for top-level destination changes (e.g., Explore -> Dashboard).When returning to the list view from an external intent (like an OAuth browser callback), the navigation graphs may become out of sync, leading to IllegalArgumentException: Destination with route cannot be found.
EmptyDetail destination to gracefully clear the view. The scaffold will interpret the empty detail state and automatically jump back to the list cleanly logic!// Example callback reset
mainNavController.navigate(Destinations.EmptyDetail) {
popUpTo(Destinations.EmptyDetail) { inclusive = true }
}
We use the official Compose Navigation 3 with Kotlin Serialization.
Routes are defined in Destinations.kt as @Serializable classes or objects.
@Serializable
object Dashboard : Destinations
@Serializable
data class ShowDetail(
val showId: Long,
val showTitle: String?
) : Destinations
navController.navigate(Destinations.ShowDetail(showId = 123, showTitle = "Arcane"))
To extract arguments (e.g., for a TopBar title), use toRoute<T>() on the NavBackStackEntry.
✅ Correct Pattern:
// AppNavigation.kt
val currentEntry = navBackStackEntry
val showTitle = if (currentEntry?.destination?.hasRoute<Destinations.ShowDetail>() == true) {
try {
currentEntry.toRoute<Destinations.ShowDetail>().showTitle
} catch (e: Exception) { null }
} else { null }
ShowDetail) when the destination matches.isDetailFlowActive) inside a LaunchedEffect that observes that same state without proper checks.!!) on LazyRow or LazyVerticalGrid items(list!!) when the underlying data stream is briefly interrupted (e.g. during a Trakt logout where the Database momentarily clears its snapshot before the Viewmodel drops the authorization state)..orEmpty() on Compose list arguments, like items(list.orEmpty()), ensuring Compose safely renders zero items instead of instantly crashing the active Activity during auth transitions.NavigationSuiteScaffold (Bottom Bar / Rail) should wrap the content.