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
currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true) to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets.
- Dialogs & Alerts: Use centralized components like
AlertHost(alertManager) from core:ui/commonMain. Do NOT trigger alerts inline or duplicate alert logic. Use SharedDialogs(uiViewModel) for general popups.
- Placeholders: Use
PlaceholderScreen(name) from core:ui/commonMain for unimplemented desktopApp/JVM features.
- Theme Picker: Use
ThemePickerDialog from feature:settings/commonMain.
- Platform Implementations: Inject platform-specific behavior (e.g., Map providers) via
CompositionLocal from the androidApp or desktopApp shells. Do not tightly couple Google Maps dependencies to commonMain; the MapLibre surfaces live in :feature:map-maplibre, not in a core module.
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 blocking getString() in a coroutine context.
- Formatting Constraints: CMP
stringResource only supports %N$s (string) and %N$d (integer).
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$s and pre-format floats.
- Prefer
MetricFormatter over scattered formatString("%.1f°C", temp) calls.
formatString (pure Kotlin) is a pure-Kotlin commonMain implementation for: hex formats, multi-arg templates fetched at runtime, and chart axis formatters. Located in core:common Formatter.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 regenerates strings-index.txt.
- Use the generated
org.meshtastic.core.resources.<key> symbol.
- Validate UI presentation.
3. Tooling & Capabilities
- Image Loading: Use
libs.coil (Coil Compose) in feature modules. Configuration/Networking for Coil (coil-network-ktor3) happens strictly in the androidApp and desktopApp host modules.
- QR Codes: Use
rememberQrCodePainter from core:ui/commonMain powered by qrcode-kotlin. No ZXing or Android Bitmap APIs in shared code.
4. Compose Previews
- Preview in commonMain: CMP 1.11+ supports
@Preview in commonMain via compose-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
rememberSaveable for dialog state (search queries, selected tabs, expanded flags) to preserve across configuration changes. Boolean and String types are auto-saveable — no custom Saver needed.
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. click addresses nodes by nodeId taken from that tree. Prefer it over take_screenshot, whose output depends on the host renderer.
reload after editing sources applies the change into the running app; use await_reload instead 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
1---2name: compose-ui3description: Skill: Compose Multiplatform (CMP) UI4---5# Skill: Compose Multiplatform (CMP) UI67## Description8Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive.910## 1. UI Components & Layouts11- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets.12- **Dialogs & Alerts:** Use centralized components like `AlertHost(alertManager)` from `core:ui/commonMain`. Do NOT trigger alerts inline or duplicate alert logic. Use `SharedDialogs(uiViewModel)` for general popups.13- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features.14- **Theme Picker:** Use `ThemePickerDialog` from `feature:settings/commonMain`.15- **Platform Implementations:** Inject platform-specific behavior (e.g., Map providers) via `CompositionLocal` from the `androidApp` or `desktopApp` shells. Do not tightly couple Google Maps dependencies to `commonMain`; the MapLibre surfaces live in `:feature:map-maplibre`, not in a `core` module.1617## 2. Strings & Resources18- **Multiplatform Resources:** MUST use `core:resources` (e.g., `stringResource(Res.string.your_key)`). Never use hardcoded strings.19- **ViewModels/Coroutines:** Use the asynchronous `getStringSuspend(Res.string.your_key)`. NEVER use blocking `getString()` in a coroutine context.20- **Formatting Constraints:** CMP `stringResource` only supports `%N$s` (string) and `%N$d` (integer).21 - **No Float formatting:** Formats like `%N$.1f` pass through unsubstituted. Pre-format in Kotlin using `NumberFormatter.format(value, decimalPlaces)` from `core:common` and pass as a string argument (`%N$s`):22 ```kotlin23 val formatted = NumberFormatter.format(batteryLevel, 1) // "73.5"24 stringResource(Res.string.battery_percent, formatted) // uses %1$s25 ```26 - **Percent Literals:** Use bare `%` (not `%%`) for literal percent signs in CMP-consumed strings.2728### String Formatting Decision Tree29Choose the right tool for the job:3031| Scenario | Tool | Example |32|----------|------|---------|33| **Metric display** (temp, voltage, %, signal) | `MetricFormatter.*` | `MetricFormatter.temperature(25.0f, isFahrenheit)` → `"77.0°F"` |34| **Simple number + unit** | `NumberFormatter` + interpolation | `"${NumberFormatter.format(val, 1)} dB"` |35| **Localized template from strings.xml** | `stringResource(Res.string.key, preFormattedArgs)` | `stringResource(Res.string.battery, formatted)` |36| **Non-composable template** (notifications, plain functions) | `formatString(template, args)` | `formatString(template, label, value)` |37| **Hex formatting** | `formatString` | `formatString("!%08x", nodeNum)` |38| **Date/time** | `DateFormatter` | `DateFormatter.format(instant)` |3940**Rules:**411. **NEVER use `%.Nf` in strings.xml** — CMP cannot substitute them. Use `%N$s` and pre-format floats.422. **Prefer `MetricFormatter`** over scattered `formatString("%.1f°C", temp)` calls.433. **`formatString` (pure Kotlin)** is a pure-Kotlin `commonMain` implementation for: hex formats, multi-arg templates fetched at runtime, and chart axis formatters. Located in `core:common` `Formatter.kt`.444. **`NumberFormatter`** always uses `.` as decimal separator — intentional for mesh networking precision.4546- **Workflow to Add a String:**47 1. Add to `core/resources/src/commonMain/composeResources/values/strings.xml`.48 2. Run `python3 scripts/sort-strings.py` — keeps the file sorted and regenerates `strings-index.txt`.49 3. Use the generated `org.meshtastic.core.resources.<key>` symbol.50 4. Validate UI presentation.5152## 3. Tooling & Capabilities53- **Image Loading:** Use `libs.coil` (Coil Compose) in feature modules. Configuration/Networking for Coil (`coil-network-ktor3`) happens strictly in the `androidApp` and `desktopApp` host modules.54- **QR Codes:** Use `rememberQrCodePainter` from `core:ui/commonMain` powered by `qrcode-kotlin`. No ZXing or Android Bitmap APIs in shared code.5556## 4. Compose Previews57- **Preview in commonMain:** CMP 1.11+ supports `@Preview` in `commonMain` via `compose-multiplatform-ui-tooling-preview`. Place preview functions alongside their composables.58- **Import:** Use `androidx.compose.ui.tooling.preview.Preview`. The JetBrains-prefixed import (`org.jetbrains.compose.ui.tooling.preview.Preview`) is deprecated.5960## 5. Dialog & State Patterns61- **Dialog State Preservation:** Use `rememberSaveable` for dialog state (search queries, selected tabs, expanded flags) to preserve across configuration changes. Boolean and String types are auto-saveable — no custom `Saver` needed.6263## 6. Driving the running desktop app64CMP 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.65- **Tools:** `status`, `reload`, `await_reload`, `get_semantic_tree`, `click`, `type_text`, `scroll`, `get_logs`, `get_ui_error`, `take_screenshot`.66- **Poll `status` until `connected: true`** before anything else — the server accepts requests before the app has connected to it.67- **`get_semantic_tree` is the assertion surface:** roles, text, `selected`/`focused`, available actions and bounds. `click` addresses nodes by `nodeId` taken from that tree. Prefer it over `take_screenshot`, whose output depends on the host renderer.68- **`reload` after editing sources** applies the change into the running app; use `await_reload` instead when the app was started with `--auto`.6970## Reference Anchors71- **Shared Strings:** `core/resources/src/commonMain/composeResources/values/strings.xml`72- **Platform abstraction contract:** `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt`73- **Provider wiring:** `androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt`