<< All versions

Skill v1.0.4

currentAutomated scan100/100
meshtastic/meshtastic-android/compose-ui

3 files

──Details
PublishedOctober 1, 2026 at 03:07 PM
Content Hashsha256:5774507ef11f447e...
Git SHA21031efe4244
Bump Typepatch
Compare with v1.0.3
──Files
Files (1 file, 7.5 KB)
SKILL.md7.5 KBactive
SKILL.md · 86 lines · 7.5 KB

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) 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.
  • Empty states: Use EmptyState(icon, title, supportingText, action) from core:ui/commonMain for an empty list or pane rather than a hand-built icon-and-text column.
  • 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).
  • 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):

``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:

ScenarioToolExample
Metric display (temp, voltage, %, signal)MetricFormatter.*MetricFormatter.temperature(25.0f, isFahrenheit) → "77.0°F"
Simple number + unitNumberFormatter + interpolation"${NumberFormatter.format(val, 1)} dB"
Localized template from strings.xmlstringResource(Res.string.key, preFormattedArgs)stringResource(Res.string.battery, formatted)
Non-composable template (notifications, plain functions)formatString(template, args)formatString(template, label, value)
Hex formattingformatStringformatString("!%08x", nodeNum)
Date/timeDateFormatterDateFormatter.format(instant)

Rules:

  1. NEVER use `%.Nf` in strings.xml — CMP cannot substitute them. Use %N$s and pre-format floats.
  2. Prefer `MetricFormatter` over scattered formatString("%.1f°C", temp) calls.
  3. `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.
  4. `NumberFormatter` always uses . as decimal separator — intentional for mesh networking precision.
  • Workflow to Add a String:
  1. Add to core/resources/src/commonMain/composeResources/values/strings.xml.
  2. Run python3 scripts/sort-strings.py — keeps the file sorted and regenerates strings-index.txt.
  3. Use the generated org.meshtastic.core.resources.<key> symbol.
  4. 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 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
← v1.0.3All versions