Navigation & Adaptive UI
Navigation & Adaptive UI Skill
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.
📱 Adaptive Layouts (List-Detail)
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.
⚠️ Critical Rule: Navigator Mismatch
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
// ...
)
Back Navigation
The scaffold handles back navigation between panes (Detail -> List) automatically if defaultBackBehavior is set correctly.
- Do NOT manually intercept back presses for pane navigation using
BackHandlerif the scaffold can handle it. - Do use
BackHandleronly for top-level destination changes (e.g., Explore -> Dashboard).
Resetting Pane State (OAuth / Deep Links)
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.
- Fix: Route the detail pane to the
EmptyDetaildestination 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 }
}
🧭 Type-Safe Navigation (Nav 3)
We use the official Compose Navigation 3 with Kotlin Serialization.
1. Defining Routes
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
2. Navigating
navController.navigate(Destinations.ShowDetail(showId = 123, showTitle = "Arcane"))
3. Extracting Arguments (The "Title Unknown" Fix)
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 }
🔧 Common Pitfalls
"Title Unknown"
- Cause: Hardcoding titles or failing to parse args from the current route.
- Fix: Always attempt to extract the specific route args (like
ShowDetail) when the destination matches.
Infinite Navigation Loops
- Cause: Updating state (like
isDetailFlowActive) inside aLaunchedEffectthat observes that same state without proper checks. - Fix: Ensure state updates only happen when the target state differs from current state.
Lazy Grid Null Pointer Exceptions (Stream Drops)
- Cause: Using aggressive non-null assertions (
!!) onLazyRoworLazyVerticalGriditems(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). - Fix: Always use safe-unwrapping
.orEmpty()on Compose list arguments, likeitems(list.orEmpty()), ensuring Compose safely renders zero items instead of instantly crashing the active Activity during auth transitions.
Bottom Bar Visibility
- Rule: The
NavigationSuiteScaffold(Bottom Bar / Rail) should wrap the content. - Logic: The list-detail scaffold lives inside the navigation suite.