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.
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive.
currentWindowAdaptiveInfoV2(), which includes the Large (1200dp) and XL (1600dp) width classes; currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true) is deprecated in its favor. Multi-pane layouts come from MeshtasticNavDisplay's list-detail scene strategy (see the navigation-and-di skill).AlertHost(alertManager) from core:ui/commonMain. Do NOT trigger alerts inline or duplicate alert logic. Use SharedDialogs(uiViewModel) for general popups.PlaceholderScreen(name) from core:ui/commonMain for unimplemented desktopApp/JVM features.EmptyState(icon, title, supportingText, action) from core:ui/commonMain for an empty list or pane rather than a hand-built icon-and-text column.ThemePickerDialog from feature:settings/commonMain.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.core:resources (e.g., stringResource(Res.string.your_key)). Never use hardcoded strings.getStringSuspend(Res.string.your_key). NEVER use blocking getString() in a coroutine context.stringResource only supports %N$s (string) and %N$d (integer).
%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):
val formatted = NumberFormatter.format(batteryLevel, 1) // "73.5"
stringResource(Res.string.battery_percent, formatted) // uses %1$s% (not %%) for literal percent signs in CMP-consumed strings.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.formatDateTime(timestampMillis) |
Rules:
%.Nf in strings.xml — CMP cannot substitute them. Use %N$s and pre-format floats.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.core/resources/src/commonMain/composeResources/values/strings.xml.python3 scripts/sort-strings.py — keeps the file sorted and regenerates strings-index.txt.org.meshtastic.core.resources.<key> symbol.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.libs.coil (Coil Compose) in feature modules. Configuration/Networking for Coil (coil-network-ktor3) happens strictly in the androidApp and desktopApp host modules.rememberQrCodePainter from core:ui/commonMain powered by qrcode-kotlin. No ZXing or Android Bitmap APIs in shared code.@Preview in commonMain via compose-multiplatform-ui-tooling-preview. Place preview functions alongside their composables.androidx.compose.ui.tooling.preview.Preview. The JetBrains-prefixed import (org.jetbrains.compose.ui.tooling.preview.Preview) is deprecated.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.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.
status, reload, await_reload, get_semantic_tree, click, type_text, scroll, get_logs, get_ui_error, take_screenshot.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.core/resources/src/commonMain/composeResources/values/strings.xmlcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.ktandroidApp/src/main/kotlin/org/meshtastic/app/MainActivity.ktmeshtastic/Meshtastic-Android
1bedc5d
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.