Skip to content
compose-expert logo

Compose Multiplatform Expert

compose-expert

Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determin...

vitorpamplona/amethyst1.4Kinstalls1.6Kstars

SKILL.md

Full skill instructions

Compose 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 commonMain or 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

  1. Uses only Material3 primitives? → Share in commonMain
  2. Requires platform system APIs? → Platform-specific
  3. Pure visual component without navigation? → Share in commonMain
  4. Needs platform UX patterns? → Ask android-expert or desktop-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
  • modifier parameter = 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 + message
  • EmptyState - Empty message + optional refresh button
  • ErrorState - 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

TaskPatternLocation
Reusable UIState hoistingcommons/​commonMain
Simple stateremember { mutableStateOf() }Composable scope
Derived statederivedStateOf { }remember block
Async → stateproduceState { }Composable function
Custom iconsroboBuilder + PathDatacommons/​icons
Loading/​ErrorLoadingState, ErrorStatecommons/​ui/​components
Theme colorsMaterialTheme.colorSchemeAny @Composable
NavigationDelegate to platform expertamethyst/, desktopApp/

Common Workflows

Creating a Shared Component

  1. Start in commons/​src/​commonMain/​kotlin/​.../​ui/​components/
  2. Use Material3 primitives only
  3. Hoist state (parameters for data, callbacks for events)
  4. Add modifier parameter
  5. Use MaterialTheme for colors/​typography
  6. Test on both Android and Desktop

Converting Existing Component

  1. Read current implementation in amethyst/ or desktopApp/
  2. Identify pure visual logic (no platform APIs)
  3. Create in commons/​commonMain with hoisted state
  4. Replace platform implementations with shared component
  5. Keep platform-specific wrappers if needed

Custom Icon

  1. Export SVG from design tool
  2. Convert to PathData using Android Studio
  3. Create icon function with roboBuilder
  4. Add caching if generated dynamically
  5. 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

More skills from vitorpamplona

feed-patterns logo
vitorpamplona/amethyst

feed-patterns

Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with `FeedFilter` / `AdditiveComplexFeedFilter` / `ChangesFlowFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or...

1.6K 0
View
quartz-integration logo
vitorpamplona/amethyst

quartz-integration

Integration guide for using the Quartz Nostr KMP library in external projects. Use when: (1) adding Quartz as a Gradle dependency, (2) setting up NostrClient with WebSocket, (3) creating/signing/sending events, (4) building relay subscriptions with Filter, (5) handling keys with KeyPair/NostrSign...

1.6K 0
View
amy-expert logo
vitorpamplona/amethyst

amy-expert

Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/` into `commons/` so a...

1.6K 0
View
auth-signers logo
vitorpamplona/amethyst

auth-signers

Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract `NostrSigner` base clas...

1.6K 0
View
gradle-expert logo
vitorpamplona/amethyst

gradle-expert

Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencie...

1.6K 0
View
account-state logo
vitorpamplona/amethyst

account-state

Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user StateFlow properties — follow list, relays, settings, mutes, bookmarks), `LocalCache` (the object-level event store backed by `LargeCache`), `User`/`Note` model classes, or any ViewModel tha...

1.6K 0
View
android-expert logo
vitorpamplona/amethyst

android-expert

Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4) Material3 theming...

1.6K 0
View
relay-client logo
vitorpamplona/amethyst

relay-client

Subscription and filter-assembly patterns for the Amethyst relay client layer in `commons/.../relayClient/`. Use when working with compose-scoped subscriptions (`ComposeSubscriptionManager`, `Subscribable`), filter assemblers (`MetadataFilterAssembler`, `ReactionsFilterAssembler`, `FeedMetadataCo...

1.6K 0
View
Amethyst logo
vitorpamplona/amethyst

Amethyst

Build customized Amethyst Nostr clients for Android. Fork, rebrand, customize, and distribute your own version.

1.6K 0
View
kotlin-coroutines logo
vitorpamplona/amethyst

kotlin-coroutines

Advanced Kotlin coroutines patterns for AmethystMultiplatform. Use when working with: (1) Structured concurrency (supervisorScope, coroutineScope), (2) Advanced Flow operators (flatMapLatest, combine, merge, shareIn, stateIn), (3) Channels and callbackFlow, (4) Dispatcher management and context s...

1.6K 0
View
kotlin-expert logo
vitorpamplona/amethyst

kotlin-expert

Advanced Kotlin patterns for AmethystMultiplatform. Flow state management (StateFlow/SharedFlow), sealed hierarchies (classes vs interfaces), immutability (@Immutable, data classes), DSL builders (type-safe fluent APIs), inline functions (reified generics, performance). Use when working with: (1)...

1.6K 0
View
find-missing-translations logo
vitorpamplona/amethyst

find-missing-translations

Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language

1.6K 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View