Skill v1.0.4
currentAutomated scan100/1003 files
version: "1.0.4" name: compose-ui description: Build shared Compose Multiplatform UI in Meshtastic-Android - adaptive layouts on Material 3 Adaptive, plus the string and resource rules. Use this whenever you add or change a composable, add a user-facing string, or work on tablet, desktop or landscape layout. Consult the bundled strings-index.txt rather than opening the raw strings.xml, which is guarded for size.
Skill: Compose Multiplatform (CMP) UI
Description
Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive.
1. UI Components & Layouts
- Material 3 / Adaptive: Use
currentWindowAdaptiveInfoV2(), which includes the Large (1200dp) and XL (1600dp) width classes;currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)is deprecated in its favour. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets. - Dialogs & Alerts: Use centralized components like
AlertHost(alertManager)fromcore:ui/commonMain. Do NOT trigger alerts inline or duplicate alert logic. UseSharedDialogs(uiViewModel)for general popups. - Placeholders: Use
PlaceholderScreen(name)fromcore:ui/commonMainfor unimplemented desktopApp/JVM features. - Empty states: Use
EmptyState(icon, title, supportingText, action)fromcore:ui/commonMainfor an empty list or pane rather than a hand-built icon-and-text column. - Theme Picker: Use
ThemePickerDialogfromfeature:settings/commonMain. - Platform Implementations: Inject platform-specific behavior (e.g., Map providers) via
CompositionLocalfrom theandroidAppordesktopAppshells. Do not tightly couple Google Maps dependencies tocommonMain; the MapLibre surfaces live in:feature:map-maplibre, not in acoremodule.
2. Strings & Resources
- Multiplatform Resources: MUST use
core:resources(e.g.,stringResource(Res.string.your_key)). Never use hardcoded strings. - ViewModels/Coroutines: Use the asynchronous
getStringSuspend(Res.string.your_key). NEVER use blockinggetString()in a coroutine context. - Formatting Constraints: CMP
stringResourceonly supports%N$s(string) and%N$d(integer). - No Float formatting: Formats like
%N$.1fpass through unsubstituted. Pre-format in Kotlin usingNumberFormatter.format(value, decimalPlaces)fromcore:commonand pass as a string argument (%N$s):
``kotlin val formatted = NumberFormatter.format(batteryLevel, 1) // "73.5" stringResource(Res.string.battery_percent, formatted) // uses %1$s ``
- Percent Literals: Use bare
%(not%%) for literal percent signs in CMP-consumed strings.
String Formatting Decision Tree
Choose the right tool for the job:
| Scenario | Tool | Example | |
|---|---|---|---|
| Metric display (temp, voltage, %, signal) | MetricFormatter.* | MetricFormatter.temperature(25.0f, isFahrenheit) → "77.0°F" | |
| Simple number + unit | NumberFormatter + interpolation | "${NumberFormatter.format(val, 1)} dB" | |
| Localized template from strings.xml | stringResource(Res.string.key, preFormattedArgs) | stringResource(Res.string.battery, formatted) | |
| Non-composable template (notifications, plain functions) | formatString(template, args) | formatString(template, label, value) | |
| Hex formatting | formatString | formatString("!%08x", nodeNum) | |
| Date/time | DateFormatter | DateFormatter.format(instant) |
Rules:
- NEVER use `%.Nf` in strings.xml — CMP cannot substitute them. Use
%N$sand pre-format floats. - Prefer `MetricFormatter` over scattered
formatString("%.1f°C", temp)calls. - `formatString` (pure Kotlin) is a pure-Kotlin
commonMainimplementation for: hex formats, multi-arg templates fetched at runtime, and chart axis formatters. Located incore:commonFormatter.kt. - `NumberFormatter` always uses
.as decimal separator — intentional for mesh networking precision.
- Workflow to Add a String:
- Add to
core/resources/src/commonMain/composeResources/values/strings.xml. - Run
python3 scripts/sort-strings.py— keeps the file sorted and regeneratesstrings-index.txt. - Use the generated
org.meshtastic.core.resources.<key>symbol. - Validate UI presentation.
- Schema strings are generated, not written. Every label and description in the protobufs field metadata is
in values/schema_strings.xml, keyed by schema path: Config.LoRaConfig.hop_limit is Res.string.schema_lora_hop_limit, its summary schema_lora_hop_limit_description, the enum value PositionFlags.DOP schema_position_positionflags_dop (all indexed under ### SCHEMA in strings-index.txt). A control that edits one whole schema field uses that key; a control that edits a bit, a threshold, a negation or drops a unit keeps a hand-written string. Never edit the generated file or write a schema_ key by hand; :schema-strings:test fails on both. Wrong wording is a protobufs change. A merged protobufs pin bump triggers a scheduled-updates run on main that regenerates the file (it records the pin it was built from); run ./gradlew :schema-strings:sync yourself only when you need a new key before that PR lands.
3. Tooling & Capabilities
- Image Loading: Use
libs.coil(Coil Compose) in feature modules. Configuration/Networking for Coil (coil-network-ktor3) happens strictly in theandroidAppanddesktopApphost modules. - QR Codes: Use
rememberQrCodePainterfromcore:ui/commonMainpowered byqrcode-kotlin. No ZXing or Android Bitmap APIs in shared code.
4. Compose Previews
- Preview in commonMain: CMP 1.11+ supports
@PreviewincommonMainviacompose-multiplatform-ui-tooling-preview. Place preview functions alongside their composables. - Import: Use
androidx.compose.ui.tooling.preview.Preview. The JetBrains-prefixed import (org.jetbrains.compose.ui.tooling.preview.Preview) is deprecated.
5. Dialog & State Patterns
- Dialog State Preservation: Use
rememberSaveablefor dialog state (search queries, selected tabs, expanded flags) to preserve across configuration changes. Boolean and String types are auto-saveable — no customSaverneeded.
6. Driving the running desktop app
CMP 1.12+ ships an MCP server inside Compose Hot Reload; .mcp.json registers it as compose-hot-reload (:desktopApp:hotMcpServer). With ./gradlew :desktopApp:hotRun running, it drives the live app — inspect, input and reload without a rebuild.
- Tools:
status,reload,await_reload,get_semantic_tree,click,type_text,scroll,get_logs,get_ui_error,take_screenshot. - Poll `status` until `connected: true` before anything else — the server accepts requests before the app has connected to it.
- `get_semantic_tree` is the assertion surface: roles, text,
selected/focused, available actions and bounds.clickaddresses nodes bynodeIdtaken from that tree. Prefer it overtake_screenshot, whose output depends on the host renderer. - `reload` after editing sources applies the change into the running app; use
await_reloadinstead when the app was started with--auto.
Reference Anchors
- Shared Strings:
core/resources/src/commonMain/composeResources/values/strings.xml - Platform abstraction contract:
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt - Provider wiring:
androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt