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 determining
By vitorpamplona · 1,240 installs
npx skills add vitorpamplona/amethyst --skill compose-expert
Source repository · Upstream listing
compose expert
Where shared Compose UI lives in Amethyst and the house rules for it. What goes in
commons vs commonsUI vs a platform shim is in AGENTS.md ("Sharing Philosophy",
"Feature Workflow") and commonsUI/ARCHITECTURE.md ; this skill does not repeat it.
Delegate: Activity / manifest / flavors android expert ; Window, tray, menu bar
desktop expert ; source set placement, expect/actual, extracting an Android composable
kotlin multiplatform ; feeds and Account state account state ; Flow and coroutine
scopes kotlin coroutines .
Where things live
All paths below are under
commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ .
What Where
Screens ui/screen/loggedIn/<feature / (and ui/screen/ for shared feed views)
Reusable components ui/components/ (catalog: references/shared composables catalog.md )
Feed scaffolding ui/feeds/ ( RenderFeedContentState , RefresheableBox , FeedEmpty , LoadingFeed )
Note rendering ui/note/ ( NoteCompose ), rich text in ui/richtext/ + ui/components/RichTextViewer.kt
Theme ui/theme/ ( AmethystColorScheme.kt palettes + ColorScheme. tokens, ColorScheme.isLight , Shape.kt , Type.kt )
Window tiers ui/layouts/ScreenLayout.kt ( ScreenLayoutSpec , LocalScreenLayout , FeedContentMaxWidth )
Shell + nav ui/app/AmethystApp.kt , ui/app/AppRoot.kt , ui/navigation/ (see below)
Platform ports for UI ui/platform/AppPlatform.kt ( LocalAppPlatform ), ui/components/PlatformPermission.kt
Icons icons/ (Amethyst glyph font) and icons/symbols/ ( MaterialSymbols , AmethystIcons )
Robohash avatars robohash/ ( CachedRobohash , RobohashAssembler , parts/ )
Strings commonsUI/src/commonMain/composeResources/values/strings.xml Res.string. (rules: AGENTS.md "Strings")
The Android app's AmethystTheme wrapper is still in
amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt ; the palettes it
applies are shared.
The shell and window tiers
One shell for every window, chosen by size, not by platform ( AGENTS.md "Navigation Shell"):
AmethystApp ( ui/app/AmethystApp.kt ) is the root both front ends call. Android's
MainActivity passes AndroidAppRoot ; the desktop window ( AmethystDesktop.kt ) passes a
DesktopAppRoot . AppRoot
( ui/app/AppRoot.kt ) is the port each platform implements: createAccountViewModel ,
rememberScreenLayoutSpec , registerDestinations , ShellOverlay , NavigationEffects , ...
ScreenLayoutSpec.navigationStyle is BOTTOM BAR , NAV RAIL or PERMANENT DRAWER ;
hasRoomForNotificationPanel docks the notification panel. isLargeScreen is "not the
bottom bar". Read it with LocalScreenLayout.current , never with a platform check.
AppShellLayout ( ui/navigation/shell/ ) lays out the bar / rail / drawer;
ui/navigation/bottombars/ holds AppBottomBar and AppNavigationRail ;
ui/navigation/deck/DeckArea.kt adds the opt in deck columns on windows of 1,200 dp+.
Navigation 3
The app owns its back stack as snapshot state; NavDisplay only renders it. No
NavController , no graph. Full rules in references/navigation.md .
Routes: @Serializable sealed class Route in
commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt .
The back stack is saved through kotlinx serialization, so every route argument must be
serializable.
NavBackStacks (same package) holds stack (never empty, starts at Route.Home ) and
savedTabs (the root entry of each bottom bar tab you left, so its ViewModels and scroll
survive). A NavStackEntry 's contentKey is "nav $id" : saved state and ViewModels key
on it, so the same route opened twice is two screens.
Nav ( ui/navigation/navs/Nav.kt ) implements INav : nav , navDrawer , newStack ,
navBottomBar , popBack , popUpTo , each after the keyboard settles ( ImeSettler ).
Register a screen once with a builder from ui/navigation/host/NavDestinations.kt . Shared
screens go in NavDestinations.sharedDestinations ( ui/navigation/host/SharedDestinations.kt );
Android only ones in androidDestinations in
amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt . The builder
picks the motion and the reading column cap:
A route nobody registered fails the first time it is opened (the desktop installs an
unavailable screen instead). Gate a menu item on canOpen(route) when a front end may lack it.
Composable conventions that differ from the default
Strings come from Res , read with stringRes(Res.string.x) ; no literal user visible text
in a shared composable, no Android R.string . Default label parameters are StringResource
(see AddButton in ui/components/ActionButtons.kt ).
UI state collections are kotlinx.collections.immutable ( ImmutableList ,
ImmutableMap , persistentListOf ), an api dependency of commonsUI . The parser output
( RichTextViewerState , ParagraphState ) and the platform ports already use them; pass those,
not List , into composables.
@Immutable / @Stable mark the state classes and port interfaces handed to composables
( ScreenLayoutSpec , AppPlatform , PlatformPermission ). Only mark a class you can keep
truly immutable.
Theme dependent values come from the color scheme : MaterialTheme.colorScheme.isLight
(memoized in AmethystColorScheme.kt ), not a hand rolled luminance check.
Icons follow AGENTS.md "Icons": icons/ .kt compile into amethyst icons.ttf and
draw through AmethystIconGlyph ; Material icons are MaterialSymbols from a subset font, so
a new codepoint needs tools/material symbols subset/subset.sh . Robohash is the one
ImageVector built asset: references/icon assets.md .
Parse rich text through CachedRichTextParser , never per recomposition:
references/rich text parsing.md .
Permissions in shared UI go through an expect fun rememberXPermission(): PlatformPermission
(see ui/components/LocationPermission.kt ; the Android actual wraps Accompanist).
Reviewing a component's public API
For slot parameters, modifier placement, and the other API shape checks, use
references/api style.md .
Performance: measure first
Feed jank in this app has been measured as GC bound and RenderThread bound, with composition a
small share of frame time. Stability annotations, derivedStateOf and deferred reads only help
when a trace shows composition is the cost. Profile a release like build (the benchmark
build type in amethyst/build.gradle.kts is minified and profileable) before you apply them, and
measure again afterwards.
Compose compiler reports are configured only for :amethyst ( composeCompiler { reportsDestination
... } near line 551 of amethyst/build.gradle.kts , written to amethyst/build/compose compiler/ ).
They cover only composables compiled in that module. Shared composables are compiled in
:commonsUI , which has no such block. To inspect their skippability, add the block to
commonsUI/build.gradle.kts temporarily, and don't commit it.
See it running
A screen change is not done until you've looked at it: tools/desktop run/run headless.sh start
runs the desktop app, which is this same UI, on a virtual display and screenshots it.
AMETHYST SCREEN=480x900x24 shows the phone tier. See tools/desktop run/README.md and
desktop expert .
References
references/navigation.md : back stack operations, rendering, transitions, intents, tests
references/shared composables catalog.md : the reusable components and their signatures
references/rich text parsing.md : RichTextParser , segments, CachedRichTextParser , NIP 92 imeta
references/icon assets.md : robohash avatars
references/api style.md : component API review checklist (slots, modifiers)