
Compose Multiplatform Expert
FreeStreamline UI component sharing across platforms.
Free · Opens the source repo
What Compose Multiplatform Expert does
Compose Multiplatform Expert provides a framework for developing and optimizing shared UI components across Android and Desktop applications. This skill is particularly useful for developers looking to create or refactor visual UI components that can be reused across different platforms without duplicating code. It emphasizes a philosophy of sharing by default, encouraging developers to place common UI elements in the commonMain directory unless platform-specific functionality is required.
The skill includes guidance on state management patterns, such as using remember, derivedStateOf, and produceState, which help in managing UI state efficiently. It also covers best practices for creating custom ImageVector icons and applying Material3 theming, ensuring that your applications maintain a consistent look and feel across platforms. Performance considerations, such as lazy lists and optimized image loading, are also addressed, making this skill a comprehensive resource for UI development.
Developers can leverage this skill when deciding whether to share UI components or keep them platform-specific, with clear decision frameworks provided for various scenarios. This includes considerations for navigation structures and system integrations that may require platform-specific implementations. By following the outlined patterns and practices, developers can enhance the maintainability and performance of their applications while reducing redundancy in their codebase.
When to use it
Use this skill when creating or refactoring shared UI components, applying Material3 theming, or optimizing state management in Compose applications.
When not to use it
This skill is not suitable for projects that require extensive platform-specific navigation or system integrations that cannot be shared.
What you can build with it
Refactoring Shared Components
When refactoring existing UI components, this skill provides guidelines to ensure they can be shared effectively across platforms.
Implementing Material3 Theming
Utilize this skill to apply Material3 theming consistently across your applications, enhancing the user experience.
Optimizing State Management
Use the state management patterns provided by this skill to improve performance and maintainability in your Compose applications.
How to install Compose Multiplatform Expert
View source1. Install with the skills CLI
npx skills add vitorpamplona/amethyst/compose-expert --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by vitorpamplonaCompose Multiplatform Expert
Visual UI patterns for sharing composables across Android and Desktop.
When to Use This Skill
- Creating or refactoring shared UI components
- Deciding whether to share UI in
commonMainor keep platform-specific - Building custom ImageVector icons (robohash pattern)
- State management: remember, derivedStateOf, produceState
- Recomposition optimization: visual usage of @Stable/@Immutable
- Material3 theming and styling
- Performance: lazy lists, image loading
Delegate to other skills:
- Navigation structure →
android-expert,desktop-expert - Kotlin state patterns (StateFlow, sealed classes) →
kotlin-expert - Build configuration →
gradle-expert
Philosophy: Share by Default
Default to commons/commonMain unless platform experts indicate otherwise.
Always Share
- UI components: Buttons, cards, lists, dialogs, inputs
- State visualization: Loading, empty, error states
- Custom icons: ImageVector assets (robohash, custom paths)
- Theme utilities: Color calculations, style helpers
- Material3 components: Any UI using Material primitives
Keep Platform-Specific
- Navigation structure: Bottom nav (Android) vs Sidebar (Desktop)
- Screen layouts: Platform-specific scaffolding
- System integrations: File pickers, notifications, share sheets
- Platform UX: Gestures, keyboard shortcuts, window management
Decision Framework
- Uses only Material3 primitives? → Share in
commonMain - Requires platform system APIs? → Platform-specific
- Pure visual component without navigation? → Share in
commonMain - Needs platform UX patterns? → Ask
android-expertordesktop-expert
If uncertain, default to sharing - easier to split later than merge.
Shared Composable Anatomy
Structure
@Composable
fun SharedComponent(
// State parameters (read-only)
data: DataClass,
isLoading: Boolean,
// Event parameters (write-only)
onAction: () -> Unit,
// Visual parameters
modifier: Modifier = Modifier,
// Optional customization
colors: ComponentColors = ComponentDefaults.colors()
) {
// Implementation
}
Pattern: State down, events up
- Parameters above modifier = required state/events
modifierparameter = layout control- Parameters below modifier = optional customization
Example: AddButton
@Composable
fun AddButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Add",
enabled: Boolean = true
) {
OutlinedButton(
modifier = modifier,
enabled = enabled,
onClick = onClick,
shape = ActionButtonShape,
contentPadding = ActionButtonPadding
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
// Shared constants for consistency
val ActionButtonShape = RoundedCornerShape(20.dp)
val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
Why this works on all platforms:
- Material3 primitives (OutlinedButton, Text)
- No platform APIs
- Configurable through parameters
- Consistent styling via shared constants
State Management Patterns
remember - Cache Across Recompositions
@Composable
fun ExpandableCard() {
var isExpanded by remember { mutableStateOf(false) }
Column {
IconButton(onClick = { isExpanded = !isExpanded }) {
Icon(
if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (isExpanded) "Collapse" else "Expand"
)
}
if (isExpanded) {
Text("Expanded content...")
}
}
}
Visual pattern: Toggle button → state changes → UI expands/collapses Use for: Simple UI state (toggles, counters, text input)
derivedStateOf - Optimize Frequent Changes
@Composable
fun ScrollToTopButton(listState: LazyListState) {
// Only recomposes when showButton changes, not every scroll pixel
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
if (showButton) {
FloatingActionButton(onClick = { /* scroll to top */ }) {
Icon(Icons.Default.ArrowUpward, null)
}
}
}
Visual pattern: Scroll position (0, 1, 2...) → boolean (show/hide) → Button visibility Use for: Input changes frequently, derived result changes rarely Performance: Prevents recomposition on every scroll event
produceState - Async to Compose State
@Composable
fun LoadUserProfile(userId: String): State<User?> {
return produceState<User?>(initialValue = null, userId) {
value = repository.fetchUser(userId)
}
}
@Composable
fun ProfileScreen(userId: String) {
val user by LoadUserProfile(userId)
when (user) {
null -> LoadingState("Loading profile...")
else -> ProfileCard(user!!)
}
}
Visual pattern: Async operation → state updates → UI reflects changes Use for: Convert Flow, LiveData, callbacks into Compose state Lifecycle: Coroutine cancelled when composable leaves composition
For Kotlin-specific state patterns (StateFlow, sealed classes), see kotlin-expert.
State Hoisting
Move state up to make composables reusable:
// ❌ Stateful - hard to test, can't control externally
@Composable
fun BadSearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
// ✅ Stateless - reusable, testable
@Composable
fun GoodSearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier
)
}
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
Column {
GoodSearchBar(query = query, onQueryChange = { query = it })
SearchResults(query = query)
}
}
Principle: State up, events down
- State:
query: String(read-only parameter) - Events:
onQueryChange: (String) -> Unit(callback parameter)
Recomposition Optimization
Visual Usage of @Immutable
Use @Immutable on data classes passed to composables:
@Immutable
data class UserProfile(val name: String, val avatar: String)
@Composable
fun ProfileCard(profile: UserProfile) {
// Only recomposes when profile instance changes
Row {
RobohashImage(robot = profile.avatar)
Text(profile.name, style = MaterialTheme.typography.titleMedium)
}
}
Visual effect: Prevents recomposition when parent recomposes with same data
Pattern: Mark parameter data classes as @Immutable
Note: For Kotlin language details on @Immutable, see kotlin-expert
Stable Parameters
// ✅ Stable - won't trigger recomposition unless colors instance changes
@Composable
fun ThemedCard(
content: String,
colors: CardColors = CardDefaults.colors(),
modifier: Modifier = Modifier
) {
Card(colors = colors, modifier = modifier) {
Text(content)
}
}
For @Stable annotation details, see kotlin-expert.
Material3 Theming
All shared composables use Material3 for consistency:
@Composable
fun ThemedComponent() {
val bg = MaterialTheme.colorScheme.background
val fg = MaterialTheme.colorScheme.onBackground
val primary = MaterialTheme.colorScheme.primary
Column(
modifier = Modifier.background(bg)
) {
Text(
"Title",
style = MaterialTheme.typography.headlineMedium,
color = fg
)
Button(
onClick = { /* ... */ },
colors = ButtonDefaults.buttonColors(containerColor = primary)
) {
Text("Action")
}
}
}
Principles:
- Colors:
MaterialTheme.colorScheme.* - Typography:
MaterialTheme.typography.* - Shapes:
MaterialTheme.shapes.*
Theme Detection
@Composable
private fun isLightTheme(): Boolean {
val background = MaterialTheme.colorScheme.background
return (background.red + background.green + background.blue) / 3 > 0.5f
}
@Composable
fun ThemedIcon() {
val isDark = !isLightTheme()
val tint = if (isDark) Color.White else Color.Black
Icon(Icons.Default.Face, null, tint = tint)
}
Custom Icons: ImageVector Pattern
Amethyst uses ImageVector for multiplatform icons.
roboBuilder DSL
fun roboBuilder(block: Builder.() -> Unit): ImageVector {
return ImageVector.Builder(
name = "Robohash",
defaultWidth = 300.dp,
defaultHeight = 300.dp,
viewportWidth = 300f,
viewportHeight = 300f
).apply(block).build()
}
Building Icons
fun customIcon(fgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
builder.addPath(pathData3, fill = Black, fillAlpha = 0.2f)
}
private val pathData1 = PathData {
moveTo(144.5f, 87.5f)
reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
lineToRelative(16.0f, 16.0f)
close()
}
@Composable
fun CustomIcon() {
Image(
painter = rememberVectorPainter(
roboBuilder {
customIcon(SolidColor(Color.Blue), this)
}
),
contentDescription = "Custom icon"
)
}
Why ImageVector?
- Pure Kotlin, no XML
- Works on Android, Desktop, iOS
- GPU-accelerated
- Type-safe
Caching Pattern
object CustomIcons {
private val cache = mutableMapOf<String, ImageVector>()
fun get(key: String): ImageVector {
return cache.getOrPut(key) {
buildIcon(key)
}
}
}
@Composable
fun CachedIcon(key: String) {
Image(imageVector = CustomIcons.get(key), contentDescription = null)
}
For detailed icon patterns, see references/icon-assets.md.
Common Visual Patterns
State Visualization
@Composable
fun DataScreen(uiState: UiState) {
when (uiState) {
is UiState.Loading -> LoadingState("Loading...")
is UiState.Empty -> EmptyState(
title = "No data",
onRefresh = { /* refresh */ }
)
is UiState.Error -> ErrorState(
message = uiState.message,
onRetry = { /* retry */ }
)
is UiState.Success -> ContentList(uiState.items)
}
}
Components (all in commons/commonMain):
LoadingState- Progress indicator + messageEmptyState- Empty message + optional refresh buttonErrorState- Error message + optional retry button
Relay Status (Amethyst Pattern)
@Composable
fun RelayStatusIndicator(connectedCount: Int) {
val statusColor = when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
tint = statusColor,
modifier = Modifier.size(16.dp)
)
Text(
"$connectedCount relay${if (connectedCount != 1) "s" else ""}",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Visual mapping:
- 0 relays → Red + X icon
- 1-2 relays → Yellow + Check icon
- 3+ relays → Green + Check icon
Placeholder Pattern
@Composable
fun PlaceholderScreen(
title: String,
description: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(title, style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(16.dp))
Text(description, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
// Specific implementations
@Composable
fun SearchPlaceholder() = PlaceholderScreen(
title = "Search",
description = "Search for users, notes, and hashtags."
)
Pattern: Generic composable + specific wrappers with preset text
Performance
Avoid Unnecessary Recomposition
// ❌ Bad - recomposes on every scroll
@Composable
fun BadButton(scrollState: ScrollState) {
if (scrollState.value > 100) {
Button(onClick = {}) { Text("Top") }
}
}
// ✅ Good - only recomposes when visibility changes
@Composable
fun GoodButton(scrollState: ScrollState) {
val show by remember { derivedStateOf { scrollState.value > 100 } }
if (show) {
Button(onClick = {}) { Text("Top") }
}
}
Lazy Lists
@Composable
fun FeedList(items: List<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
FeedItem(item)
}
}
}
Key principle: Use key parameter for stable item identity
Bundled Resources
- references/shared-composables-catalog.md - Complete catalog of shared UI components
- references/state-patterns.md - State management patterns with visual examples
- references/icon-assets.md - Custom ImageVector icon patterns
- references/rich-text-parsing.md -
RichTextParser,UrlParser,GalleryParser,Patterns,MediaContentModels; NIP-92 imeta enrichment - scripts/find-composables.sh - Find all @Composable functions in codebase
Quick Reference
| Task | Pattern | Location |
|---|---|---|
| Reusable UI | State hoisting | commons/commonMain |
| Simple state | remember { mutableStateOf() } | Composable scope |
| Derived state | derivedStateOf { } | remember block |
| Async → state | produceState { } | Composable function |
| Custom icons | roboBuilder + PathData | commons/icons |
| Loading/Error | LoadingState, ErrorState | commons/ui/components |
| Theme colors | MaterialTheme.colorScheme | Any @Composable |
| Navigation | Delegate to platform expert | amethyst/, desktopApp/ |
Common Workflows
Creating a Shared Component
- Start in
commons/src/commonMain/kotlin/.../ui/components/ - Use Material3 primitives only
- Hoist state (parameters for data, callbacks for events)
- Add modifier parameter
- Use MaterialTheme for colors/typography
- Test on both Android and Desktop
Converting Existing Component
- Read current implementation in
amethyst/ordesktopApp/ - Identify pure visual logic (no platform APIs)
- Create in
commons/commonMainwith hoisted state - Replace platform implementations with shared component
- Keep platform-specific wrappers if needed
Custom Icon
- Export SVG from design tool
- Convert to PathData using Android Studio
- Create icon function with roboBuilder
- Add caching if generated dynamically
- Wrap in @Composable for easy use
Navigation (Delegate)
For navigation patterns:
- Android bottom nav →
android-expert - Desktop sidebar →
desktop-expert - Multi-window →
desktop-expert
Related Skills
- kotlin-expert - Kotlin language aspects (@Immutable details, StateFlow, sealed classes)
- android-expert - Android navigation, platform APIs
- desktop-expert - Desktop navigation, window management, OS specifics
- kotlin-coroutines - Async patterns, Flow integration
Frequently asked questions about Compose Multiplatform Expert
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
