Compose conventions
Skill thetruong1099/android-mvi-base-code/.claude/skills/compose-conventions
Jetpack Compose UI conventions for this project's screens. Use when creating composable screens (XxxScreen/XxxScreenInternal pattern), using BaseScreenComponent, applying MaterialTheme.spacing for padding, writing @Preview composables with FakeBaseViewModel, handling PagingData with rememberPagingRefreshState, or setting up navigation with LocalAppNavController.From its SKILL.md
npx -y skills add thetruong1099/android-mvi-base-code --skill compose-conventionsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.0 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
Compose Conventions
Screen Structure Pattern
XxxScreen() -> Stateful: DI (hiltViewModel), effects, navigation
XxxScreenInternal() -> Stateless: previewable, uses IBaseViewModel interface
ContentComponent() -> Private: actual UI content
Stateful Screen
@Composable
fun XxxScreen(viewModel: XxxViewModel = hiltViewModel()) {
val navController = LocalAppNavController.current
LaunchedEffect(Unit) {
viewModel.effectState.collect { effect ->
when (effect) {
is XxxViewEffect.NavigateToY -> {
navController.navigateTo(Screen.YScreen(...))
viewModel.clearEffect()
}
null -> {}
}
}
}
XxxScreenInternal(
viewModel = viewModel,
onAction = { viewModel.onTriggerEvent(XxxViewEvent.OnAction) },
)
}
Stateless Screen
@Composable
internal fun XxxScreenInternal(
viewModel: IBaseViewModel<XxxViewState, XxxViewEvent, XxxViewEffect>,
modifier: Modifier = Modifier,
onAction: () -> Unit = {},
) {
BaseScreenComponent(
viewModel = viewModel,
modifier = modifier.fillMaxSize().statusBarsPadding(),
topBar = { state -> XxxTopAppBar(...) },
) { state, paddingValues ->
// Content using state
}
}
BaseScreenComponent
BaseScreenComponent(
viewModel = viewModel,
modifier = modifier,
enableLoading = true,
externalIsRefreshing = refreshState.isRefreshing,
onRefreshListener = { refreshState.refresh() },
onEffect = { effect -> /* handle effects (Pattern 2) */ },
topBar = { state -> /* TopAppBar */ },
bottomBar = { /* BottomBar */ },
floatingActionButton = { /* FAB */ },
diaLogComposable = { state -> /* Dialogs/BottomSheets */ },
loadingComposable = { CircleLoadingComponent() },
) { state, paddingValues ->
// Main content
}
Features: state collection, effect handling, pull-to-refresh, loading overlay, system bar colors, keyboard dismiss, back button.
CompositionLocals
| Local | Type | Usage |
|---|---|---|
LocalAppNavController | NavHostController | LocalAppNavController.current |
LocalToastHostState | ToastHostState | Toast display |
LocalSpacing | Spacing | MaterialTheme.spacing.dp16 |
Spacing System
// CORRECT
Modifier.padding(MaterialTheme.spacing.dp16)
Modifier.padding(horizontal = MaterialTheme.spacing.dp16, vertical = MaterialTheme.spacing.dp8)
// INCORRECT
Modifier.padding(16.dp) // Don't hardcode
Values: dp0, dp1, dp2, dp4, dp6, dp8, dp10, dp12, dp14, dp16, dp20, dp24, dp32, dp40,
dp48, dp56, dp64, dp80, dp96, dp120, dp160, dp200, dp500
Navigation
// Route definition in feature/core/navigation/routes/NavigationRoutes.kt
sealed class Screen {
@Serializable data object SampleScreen : Screen()
@Serializable data class DetailScreen(val id: String) : Screen()
}
// Navigation extensions
navController.navigateTo(Screen.DetailScreen(id = "123"))
navController.navigateAndClearBackStack(Screen.SampleScreen)
navController.navigateSingleTop(Screen.SampleScreen)
navController.safePopBackStack()
// NavGraphBuilder extension in feature/xxx/XxxNavigation.kt
fun NavGraphBuilder.xxxScreen() {
composable<Screen.XxxScreen>(
enterTransition = slideInRight(),
exitTransition = slideOutRight(),
) {
XxxScreen()
}
}
Theme Usage
// Production - provides NavController, ToastHost, Spacing, MaterialTheme
TemplateTheme(darkTheme = isSystemInDarkTheme()) { AppContent() }
// Preview - NO NavController, NO ToastHost
TemplateThemePreview(darkTheme = false) { XxxScreenInternal(viewModel = fakeViewModel) }
Preview Pattern
@Preview(showBackground = true)
@Composable
private fun XxxScreenPreview() {
val fakeViewModel = FakeBaseViewModel<XxxViewState, XxxViewEvent, XxxViewEffect>(
initialState = XxxViewState(/* preview data */),
// initialLoading = true,
// initialEffect = XxxViewEffect.ShowDialog,
)
TemplateThemePreview(darkTheme = false) {
XxxScreenInternal(viewModel = fakeViewModel)
}
}
PagingData in Compose
@Composable
internal fun XxxScreenInternal(viewModel: IBaseViewModel<...>) {
val refreshState = rememberPagingRefreshState<ItemType>()
BaseScreenComponent(
viewModel = viewModel,
externalIsRefreshing = refreshState.isRefreshing,
onRefreshListener = { refreshState.refresh() },
) { state, padding ->
val items = state.pagingFlow?.collectAsLazyPagingItems()
refreshState.bind(items)
PagingVerticalGridComponent(
items = items,
columns = GridCells.Fixed(2),
) { pagingItems ->
items(
count = pagingItems.itemCount,
key = pagingItems.itemKey { it.id }, // Stable keys!
) { index ->
pagingItems[index]?.let { item -> ItemComponent(item = item) }
}
}
}
}
Reusable Components (feature/core/component/)
CircleLoadingComponent- Loading indicatorPagingVerticalGridComponent- Paginated gridPagingLazyColumnComponent- Paginated listToastHost+ToastHostState- Custom toast systemBaseScreenComponent- Screen wrapperNoDataComponent- Empty stateSkeletonLoaderComponent- Skeleton loading
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.