From ecc
KMP 프로젝트를 위한 Compose Multiplatform 및 Jetpack Compose 패턴 - 상태 관리, 네비게이션, 테마 설정, 성능 및 플랫폼별 UI.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
Compose Multiplatform 및 Jetpack Compose를 사용하여 안드로이드, iOS, 데스크톱 및 웹에서 공유 UI를 구축하기 위한 패턴입니다. 상태 관리, 네비게이션, 테마 설정 및 성능을 다룹니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Compose Multiplatform 및 Jetpack Compose를 사용하여 안드로이드, iOS, 데스크톱 및 웹에서 공유 UI를 구축하기 위한 패턴입니다. 상태 관리, 네비게이션, 테마 설정 및 성능을 다룹니다.
화면 상태를 위해 단일 데이터 클래스를 사용하세요. 이를 StateFlow로 노출하고 Compose에서 수집(collect)합니다.
data class ItemListState(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val searchQuery: String = ""
)
class ItemListViewModel(
private val getItems: GetItemsUseCase
) : ViewModel() {
private val _state = MutableStateFlow(ItemListState())
val state: StateFlow<ItemListState> = _state.asStateFlow()
fun onSearch(query: String) {
_state.update { it.copy(searchQuery = query) }
loadItems(query)
}
private fun loadItems(query: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
getItems(query).fold(
onSuccess = { items -> _state.update { it.copy(items = items, isLoading = false) } },
onFailure = { e -> _state.update { it.copy(error = e.message, isLoading = false) } }
)
}
}
}
@Composable
fun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
ItemListContent(
state = state,
onSearch = viewModel::onSearch
)
}
@Composable
private fun ItemListContent(
state: ItemListState,
onSearch: (String) -> Unit
) {
// 상태가 없는(Stateless) 컴포저블 - 프리뷰 및 테스트가 용이함
}
복잡한 화면의 경우 여러 개의 콜백 람다 대신 이벤트를 위한 Sealed 인터페이스를 사용하세요.
sealed interface ItemListEvent {
data class Search(val query: String) : ItemListEvent
data class Delete(val itemId: String) : ItemListEvent
data object Refresh : ItemListEvent
}
// ViewModel에서
fun onEvent(event: ItemListEvent) {
when (event) {
is ItemListEvent.Search -> onSearch(event.query)
is ItemListEvent.Delete -> deleteItem(event.itemId)
is ItemListEvent.Refresh -> loadItems(_state.value.searchQuery)
}
}
// 컴포저블에서 - 여러 개 대신 단일 람다 사용
ItemListContent(
state = state,
onEvent = viewModel::onEvent
)
라우트를 @Serializable 객체로 정의하세요.
@Serializable data object HomeRoute
@Serializable data class DetailRoute(val id: String)
@Serializable data object SettingsRoute
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController, startDestination = HomeRoute) {
composable<HomeRoute> {
HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) })
}
composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>()
DetailScreen(id = route.id)
}
composable<SettingsRoute> { SettingsScreen() }
}
}
명령형 show/hide 대신 dialog() 및 오버레이 패턴을 사용하세요.
NavHost(navController, startDestination = HomeRoute) {
composable<HomeRoute> { /* ... */ }
dialog<ConfirmDeleteRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ConfirmDeleteRoute>()
ConfirmDeleteDialog(
itemId = route.itemId,
onConfirm = { navController.popBackStack() },
onDismiss = { navController.popBackStack() }
)
}
}
유연성을 위해 슬롯 파라미터를 사용하여 컴포저블을 설계하세요.
@Composable
fun AppCard(
modifier: Modifier = Modifier,
header: @Composable () -> Unit = {},
content: @Composable ColumnScope.() -> Unit,
actions: @Composable RowScope.() -> Unit = {}
) {
Card(modifier = modifier) {
Column {
header()
Column(content = content)
Row(horizontalArrangement = Arrangement.End, content = actions)
}
}
}
Modifier 순서는 중요합니다 - 다음 순서로 적용하세요.
Text(
text = "Hello",
modifier = Modifier
.padding(16.dp) // 1. 레이아웃 (padding, size)
.clip(RoundedCornerShape(8.dp)) // 2. 모양 (shape)
.background(Color.White) // 3. 그리기 (background, border)
.clickable { } // 4. 상호작용 (interaction)
)
// commonMain
@Composable
expect fun PlatformStatusBar(darkIcons: Boolean)
// androidMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
val systemUiController = rememberSystemUiController()
SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) }
}
// iosMain
@Composable
actual fun PlatformStatusBar(darkIcons: Boolean) {
// iOS는 UIKit 상호운용성 또는 Info.plist를 통해 이를 처리합니다.
}
모든 프로퍼티가 안정적일 때 클래스를 @Stable 또는 @Immutable로 표시하세요.
@Immutable
data class ItemUiModel(
val id: String,
val title: String,
val description: String,
val progress: Float
)
key() 및 Lazy 리스트를 올바르게 사용하기LazyColumn {
items(
items = items,
key = { it.id } // 안정적인 키는 아이템 재사용 및 애니메이션을 가능하게 함
) { item ->
ItemRow(item = item)
}
}
derivedStateOf를 사용하여 읽기 지연시키기val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
// 나쁨 — 매 리컴포지션마다 새로운 람다와 리스트 생성
items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }
// 좋음 — 각 아이템에 키를 부여하여 콜백이 올바른 행에 유지되도록 함
val activeItems = remember(items) { items.filter { it.isActive } }
activeItems.forEach { item ->
key(item.id) {
ActiveItem(item, onClick = { handle(item) })
}
}
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
else dynamicLightColorScheme(LocalContext.current)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(colorScheme = colorScheme, content = content)
}
MutableStateFlow와 collectAsStateWithLifecycle 대신 ViewModel에서 mutableStateOf 사용NavController 전달 - 대신 람다 콜백 전달@Composable 함수 내부에서 무거운 계산 수행 - ViewModel이나 remember {}로 이동LaunchedEffect(Unit) 사용 - 일부 설정에서 구성 변경 시 재실행될 수 있음모듈 구조 및 레이어링은 android-clean-architecture 스킬을 참조하세요.
코루틴 및 Flow 패턴은 kotlin-coroutines-flows 스킬을 참조하세요.