compose
3
总安装量
2
周安装量
#61885
全站排名
安装命令
npx skills add https://github.com/andvl1/claude-plugin --skill compose
Agent 安装分布
opencode
2
gemini-cli
2
claude-code
2
github-copilot
2
codex
2
kimi-cli
2
Skill 文档
Compose Multiplatform
Declarative UI framework for Android, iOS, Desktop, and Web with shared code.
Setup
build.gradle.kts (Compose module)
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
jvm("desktop")
@OptIn(ExperimentalWasmDsl::class)
wasmJs { browser() }
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
}
androidMain.dependencies {
implementation(compose.uiTooling)
implementation(libs.androidx.activity.compose)
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
}
}
compose.resources {
publicResClass = true
packageOfResClass = "com.your-project.admin.resources"
generateResClass = auto
}
Resources
Directory Structure
src/commonMain/composeResources/
âââ drawable/ # Images (PNG, WebP, SVG)
â âââ ic_logo.xml # Vector drawable
â âââ bg_pattern.png
âââ drawable-dark/ # Dark theme variants
âââ font/ # TTF/OTF fonts
â âââ Inter-Regular.ttf
â âââ Inter-Bold.ttf
âââ values/
â âââ strings.xml # Default strings
âââ values-ru/
â âââ strings.xml # Russian strings
âââ files/ # Raw files
âââ config.json
strings.xml Format
<!-- values/strings.xml -->
<resources>
<string name="app_name">My Application</string>
<string name="welcome_message">Welcome, %1$s!</string>
<string name="items_count">%1$d items</string>
</resources>
<!-- values-ru/strings.xml -->
<resources>
<string name="app_name">Ðое ÐÑиложение</string>
<string name="welcome_message">ÐобÑо пожаловаÑÑ, %1$s!</string>
<string name="items_count">%1$d ÑлеменÑов</string>
</resources>
Using Resources
import com.your-project.admin.resources.Res
import com.your-project.admin.resources.*
import org.jetbrains.compose.resources.*
@Composable
fun ResourcesDemo() {
// Strings
val appName = stringResource(Res.string.app_name)
val welcome = stringResource(Res.string.welcome_message, userName)
val count = stringResource(Res.string.items_count, itemCount)
// Images
Image(
painter = painterResource(Res.drawable.ic_logo),
contentDescription = "Logo"
)
// Fonts
val typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily(Font(Res.font.Inter_Regular))
),
titleLarge = TextStyle(
fontFamily = FontFamily(Font(Res.font.Inter_Bold, FontWeight.Bold))
)
)
}
// Async resource loading (for files)
@Composable
fun ConfigLoader() {
var config by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
config = Res.readBytes("files/config.json").decodeToString()
}
}
Theme
Color Scheme
// core/ui/src/commonMain/kotlin/theme/Theme.kt
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
error = Color(0xFFCF6679)
)
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
background = Color(0xFFFAFAFA),
surface = Color.White,
error = Color(0xFFB00020)
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
Typography
// core/ui/src/commonMain/kotlin/theme/Type.kt
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily(Font(Res.font.Inter_Bold)),
fontSize = 57.sp,
lineHeight = 64.sp
),
headlineMedium = TextStyle(
fontFamily = FontFamily(Font(Res.font.Inter_Bold)),
fontSize = 28.sp,
lineHeight = 36.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily(Font(Res.font.Inter_Regular)),
fontSize = 16.sp,
lineHeight = 24.sp
),
labelLarge = TextStyle(
fontFamily = FontFamily(Font(Res.font.Inter_Regular)),
fontSize = 14.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Medium
)
)
Shapes
// core/ui/src/commonMain/kotlin/theme/Shapes.kt
val AppShapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(24.dp)
)
Component Patterns
Base Component
// core/ui/src/commonMain/kotlin/components/AppButton.kt
@Composable
fun AppButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
loading: Boolean = false,
style: ButtonStyle = ButtonStyle.Primary
) {
Button(
onClick = onClick,
modifier = modifier.height(48.dp),
enabled = enabled && !loading,
colors = when (style) {
ButtonStyle.Primary -> ButtonDefaults.buttonColors()
ButtonStyle.Secondary -> ButtonDefaults.outlinedButtonColors()
ButtonStyle.Destructive -> ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
}
) {
if (loading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
} else {
Text(text)
}
}
}
enum class ButtonStyle { Primary, Secondary, Destructive }
Card Component
@Composable
fun AppCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
val cardModifier = if (onClick != null) {
modifier.clickable(onClick = onClick)
} else {
modifier
}
Card(
modifier = cardModifier,
shape = MaterialTheme.shapes.medium,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
content = content
)
}
}
Loading State Component
@Composable
fun LoadingContent(
isLoading: Boolean,
modifier: Modifier = Modifier,
loadingContent: @Composable () -> Unit = { DefaultLoadingIndicator() },
content: @Composable () -> Unit
) {
Box(modifier = modifier) {
if (isLoading) {
loadingContent()
} else {
content()
}
}
}
@Composable
private fun DefaultLoadingIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
Error State Component
@Composable
fun ErrorContent(
message: String,
onRetry: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
if (onRetry != null) {
Spacer(modifier = Modifier.height(24.dp))
AppButton(
text = stringResource(Res.string.retry),
onClick = onRetry
)
}
}
}
Screen Pattern
// feature/home/impl/src/commonMain/kotlin/HomeScreen.kt
@Composable
fun HomeScreen(
component: HomeComponent,
modifier: Modifier = Modifier
) {
val state by component.state.subscribeAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(Res.string.home_title)) }
)
},
modifier = modifier
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
when (val currentState = state) {
is HomeState.Loading -> LoadingContent(isLoading = true) {}
is HomeState.Error -> ErrorContent(
message = currentState.message,
onRetry = component::retry
)
is HomeState.Success -> HomeContent(
data = currentState.data,
onItemClick = component::onItemClick
)
}
}
}
}
@Composable
private fun HomeContent(
data: List<HomeItem>,
onItemClick: (HomeItem) -> Unit
) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(data, key = { it.id }) { item ->
HomeItemCard(
item = item,
onClick = { onItemClick(item) }
)
}
}
}
Platform Adaptations
Safe Area Handling
@Composable
fun SafeAreaScreen(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
) {
content()
}
}
// Or in Scaffold
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { paddingValues ->
// Content
}
Platform-Specific UI
@Composable
expect fun BackHandler(enabled: Boolean, onBack: () -> Unit)
// androidMain
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack)
}
// iosMain (no back handler on iOS)
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
// No-op on iOS
}
// desktopMain
@Composable
actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
// Handle keyboard shortcut or window close
}
Adaptive Layout
@Composable
fun AdaptiveLayout(
compactContent: @Composable () -> Unit,
expandedContent: @Composable () -> Unit
) {
BoxWithConstraints {
if (maxWidth < 600.dp) {
compactContent()
} else {
expandedContent()
}
}
}
// Usage
AdaptiveLayout(
compactContent = { PhoneLayout() },
expandedContent = { TabletLayout() }
)
Entry Points
Android
// composeApp/src/androidMain/kotlin/MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootComponent = DefaultRootComponent(
componentContext = defaultComponentContext()
)
setContent {
AppTheme {
RootContent(component = rootComponent)
}
}
}
}
iOS
// composeApp/src/iosMain/kotlin/MainViewController.kt
fun MainViewController(): UIViewController {
return ComposeUIViewController {
val rootComponent = remember {
DefaultRootComponent(
componentContext = DefaultComponentContext(
lifecycle = ApplicationLifecycle()
)
)
}
AppTheme {
RootContent(component = rootComponent)
}
}
}
Desktop
// composeApp/src/desktopMain/kotlin/Main.kt
fun main() = application {
val lifecycle = LifecycleRegistry()
val rootComponent = runOnUiThread {
DefaultRootComponent(
componentContext = DefaultComponentContext(lifecycle)
)
}
Window(
onCloseRequest = ::exitApplication,
title = "My Application"
) {
LifecycleController(lifecycle)
AppTheme {
RootContent(component = rootComponent)
}
}
}
Web (WASM)
// composeApp/src/wasmJsMain/kotlin/Main.kt
fun main() {
val lifecycle = LifecycleRegistry()
val rootComponent = DefaultRootComponent(
componentContext = DefaultComponentContext(lifecycle)
)
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
AppTheme {
RootContent(component = rootComponent)
}
}
}
Best Practices
Do’s
- Use Material3 components and theme
- Keep composables stateless when possible
- Use
rememberandderivedStateOffor performance - Extract reusable components to core:ui
- Use string resources for all user-visible text
- Handle all UI states (loading, error, empty, success)
- Use WindowInsets for safe areas
Don’ts
- Don’t use hardcoded colors or dimensions
- Don’t put business logic in composables
- Don’t ignore preview annotations
- Don’t skip accessibility (contentDescription)
- Don’t use platform-specific APIs directly in common code
- Don’t create composables with side effects without LaunchedEffect
Previews
@Preview
@Composable
private fun AppButtonPreview() {
AppTheme {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
AppButton(text = "Primary", onClick = {})
AppButton(text = "Loading", onClick = {}, loading = true)
AppButton(text = "Disabled", onClick = {}, enabled = false)
AppButton(text = "Destructive", onClick = {}, style = ButtonStyle.Destructive)
}
}
}