# 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.

- Skill: `thetruong1099/compose-conventions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thetruong1099/compose-conventions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thetruong1099/compose-conventions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thetruong1099 (https://skillmd.com/u/thetruong1099)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thetruong1099/compose-conventions

---


# Compose Conventions

## Screen Structure Pattern

```
XxxScreen()            -> Stateful: DI (hiltViewModel), effects, navigation
XxxScreenInternal()    -> Stateless: previewable, uses IBaseViewModel interface
ContentComponent()     -> Private: actual UI content
```

### Stateful Screen

```kotlin
@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

```kotlin
@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

```kotlin
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

```kotlin
// 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

```kotlin
// 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

```kotlin
// Production - provides NavController, ToastHost, Spacing, MaterialTheme
TemplateTheme(darkTheme = isSystemInDarkTheme()) { AppContent() }

// Preview - NO NavController, NO ToastHost
TemplateThemePreview(darkTheme = false) { XxxScreenInternal(viewModel = fakeViewModel) }
```

## Preview Pattern

```kotlin
@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

```kotlin
@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 indicator
- `PagingVerticalGridComponent` - Paginated grid
- `PagingLazyColumnComponent` - Paginated list
- `ToastHost` + `ToastHostState` - Custom toast system
- `BaseScreenComponent` - Screen wrapper
- `NoDataComponent` - Empty state
- `SkeletonLoaderComponent` - Skeleton loading

