一键导入
navigation-adaptive-ui
Best practices for Type-Safe Navigation (Nav 3) and Adaptive Layouts in Upnext.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Best practices for Type-Safe Navigation (Nav 3) and Adaptive Layouts in Upnext.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Best practices for Android instrumentation tests, Hilt testing, Mockito configuration, and test resilience patterns in Upnext.
Strict guidelines and workflow for developing new features, ensuring quality, proper branching, UI/UX standards, and comprehensive testing in Upnext.
Expert capabilities for managing Upnext application releases, CI/CD pipelines, and versioning.
Guidelines and strict rules for Kotlin code formatting, specifically concerning imports, fully qualified names, and avoiding codebase pollution.
Core product design and development principles for Upnext, derived from competitive analysis of the official Trakt app's negative user feedback. Use this skill when proposing new features, designing UI/UX, or prioritizing the roadmap.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications.
| name | Navigation & Adaptive UI |
| description | 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: String?,
val showTitle: String?
) : Destinations
Under Navigation 3, the back stack is developer-owned, typically managed via a SnapshotStateList<Any>:
// Navigate by adding keys directly to the back stack state list
backStack.add(Destinations.ShowDetail(showId = "123", showTitle = "Arcane"))
Because the back stack keys themselves are the typed route instances, extracting arguments (e.g., for a TopBar title) is simple pattern matching:
✅ Correct Pattern:
// AppNavigation.kt
val currentKey = backStack.lastOrNull()
val showTitle = when (currentKey) {
is Destinations.ShowDetail -> currentKey.showTitle
is Destinations.PersonDetail -> currentKey.personName
else -> null
}
In Navigation 3, the runtime does not automatically bundle and inject the route arguments into the default SavedStateHandle. Calling toRoute<T>() in the ViewModel constructor will crash with a deserialization failure.
✅ Correct Pattern:
Use @AssistedInject and @AssistedFactory in your ViewModels, and resolve them using the creationCallback on hiltViewModel:
// ViewModel
class EpisodeDetailViewModel @AssistedInject constructor(
@Assisted val route: Destinations.EpisodeDetail,
// ...
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(route: Destinations.EpisodeDetail): EpisodeDetailViewModel
}
}
// Screen Composable
val viewModel: EpisodeDetailViewModel = hiltViewModel(
creationCallback = { factory: EpisodeDetailViewModel.Factory ->
factory.create(episodeDetailArg)
}
)
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.