The Android bridge described in docs/notes/android.md.
An app hands over a Mermaid source string; MermaidNative parses it natively
(JNI → the Swift mmk_* C ABI in libmermaidkit.so) into a platform-free
SceneWire scene, and SceneRenderer draws it with a real Android
Canvas/Paint.
flowchart LR
src["source<br/>(app)"] --> native["MermaidNative<br/>(this module)"]
native -->|"JNI"| core["mmk_scene_json<br/>(Swift core · libmermaidkit.so)"]
core --> wire["SceneWire JSON<br/>(the contract)"]
wire --> parse["SceneWire.parse<br/>(@Serializable, no glue)"]
parse --> draw["SceneRenderer.draw(scene, canvas)"]
The library ships as an .aar with prebuilt native .so for all three ABIs
(arm64-v8a, armeabi-v7a, x86_64) — the consumer never touches Swift, the
NDK, or JNI.
// build.gradle.kts
implementation("ai.2389:mermaidkit-android:0.1.0")Until it's on Maven Central, publish it to your local Maven repo first (the AAR's
native libs are cross-compiled in Docker, so Docker + a native x86_64 host are
needed to build it — see android/native/):
# from android/ — build all three ABIs, then assemble + publish the AAR. The
# container is swift:6.2.0 with the finagolfin Swift Android SDK installed (the
# same recipe CI and scripts/check-android-build.sh use):
BUNDLE=https://github.com/finagolfin/swift-android-sdk/releases/download/6.2/swift-6.2-RELEASE-android-24-0.1.artifactbundle.tar.gz
for abi in arm64-v8a armeabi-v7a x86_64; do
docker run --rm -v "$PWD/..":/MermaidKit \
-v "$PWD/mermaidkit/src/main/jniLibs/$abi":/out \
swift:6.2.0 bash -c "swift sdk install '$BUNDLE' >/dev/null 2>&1 && \
bash /MermaidKit/android/native/build-jni.sh $abi"
done
./gradlew :mermaidkit:publishReleasePublicationToMavenLocal # → mavenLocal()Size: one ABI's native payload is ~64 MB (the Swift runtime + Foundation/ICU), stripped. With per-ABI APK splits or an app bundle, a device downloads only its own ABI. Trimming the Foundation/ICU surface is the main size lever, and a follow-up.
Because RenderScene flattens all
30 diagram types into a tiny universal vocabulary (shape / polyline / text + a
theme), this module never needs to know what a "sequence diagram" is — it just
paints primitives in painter's order, exactly like the SVG backend.
Compose:
MermaidDiagram(
source = "flowchart LR\n A[Start] --> B{Choice}\n B -->|yes| C((Done))",
modifier = Modifier.fillMaxWidth(),
) // sizes to width, uses the app's MaterialTheme colors (light/dark),
// narration → contentDescription. Override with `theme = MermaidTheme.fromMaterial(...)`.Classic View (no Compose dependency):
val view = MermaidView(context)
view.source = "flowchart LR\n A[Start] --> B[End]" // that's it — parses, sizes, drawsBoth go source → native parse → SceneWire → Canvas, measure text with the
drawing Paint, and set the diagram's accessibility narration as
contentDescription. Both follow the system font size
(Configuration.fontScale): text is laid out larger and the boxes grow around
it, as Dynamic Type does on iOS. Pass textScale to MermaidDiagram, or set
MermaidView.textScale, to pick a size yourself. For lower-level control,
MermaidNative.scene(source, measurer = PaintMeasurer(paint), textScale = 1.3f)
returns a SceneWire you draw yourself with SceneRenderer.
Above: a flowchart drawn by SceneRenderer through a real android-34 emulator's
Skia Canvas, from the SceneWire JSON the Swift pipeline emitted — captured by
the instrumented test (connectedDebugAndroidTest).
mermaidkit/src/main/kotlin/ai/mermaidkit/scene/SceneWire.kt— the wire model: plain@Serializabledata classes with atypediscriminator. Deserializes the exact JSON the C ABI emits with zero custom serializers.mermaidkit/src/main/kotlin/ai/mermaidkit/scene/SceneRenderer.kt— draws aSceneWireonto aCanvas: rounded rects / ellipses / polygons / arbitrary paths, stroked+arrowed edge polylines, and centered/rotated text with backing chips. Colors are#RRGGBBAA(8-bit, what Skia draws at); text is measured with the samePaintthat draws it (the measure seam the C ABI callback pins).mermaidkit/src/test/…/SceneWireTest.kt— JVM unit tests that parse golden JSON captured from the real pipeline (src/test/resources/*.json).mermaidkit/src/main/kotlin/ai/mermaidkit/MermaidNative.kt— the native bridge:System.loadLibrary("mermaidkit")+external funs over themmk_*C ABI.MermaidNative.scene(source)goes straight from a source string to aSceneWire.PaintMeasurer.ktbacks the measure seam with an AndroidPaint.mermaidkit/src/main/kotlin/ai/mermaidkit/MermaidView.kt/MermaidDiagram.kt— the snap-in surface: a classicView(no Compose dep) and a@Composable, each rendering a source string in one line (see above).mermaidkit/src/main/kotlin/ai/mermaidkit/MermaidTheme.kt/MermaidThemeMaterial.kt— the theme colors sent over the ABI andMermaidTheme.fromMaterial(colorScheme).native/— the JNI native side:Sources/MermaidJNI/mermaidkit_jni.c(the C shim) and its ownPackage.swift.build-jni.shcross-compiles it with the Swift Android SDK intolibmermaidkit.so(the shim +MermaidKitClinked in) plus the transitive Swift-runtime.soclosure — the AAR'sjniLibs/<abi>/.mermaidkit/src/androidTest/…/RenderInstrumentedTest.kt— draws a scene through the emulator's SkiaCanvasand asserts real ink lands.mermaidkit/src/androidTest/…/NativeBridgeTest.kt— the full seam on-device: source string → JNI → Swift →SceneWire→ render.mermaidkit/src/androidTest/…/MermaidViewTest.kt/MermaidDiagramTest.kt— the snap-in surfaces on-device (render, size, narration).
# 1. Cross-compile the native .so bundle into jniLibs (needs Docker; per ABI).
# Container = swift:6.2.0 + the finagolfin Swift Android SDK (as in CI):
BUNDLE=https://github.com/finagolfin/swift-android-sdk/releases/download/6.2/swift-6.2-RELEASE-android-24-0.1.artifactbundle.tar.gz
docker run --rm -v "$PWD/..":/MermaidKit \
-v "$PWD/mermaidkit/src/main/jniLibs/x86_64":/out \
swift:6.2.0 bash -c "swift sdk install '$BUNDLE' >/dev/null 2>&1 && \
bash /MermaidKit/android/native/build-jni.sh x86_64"
# 2. Library AAR + JVM unit tests + instrumented-test APK (no device needed):
./gradlew :mermaidkit:assembleDebug :mermaidkit:testDebugUnitTest :mermaidkit:assembleDebugAndroidTest
# 3. On-device tests — render + the native JNI seam (needs an emulator, i.e. KVM):
./gradlew :mermaidkit:connectedDebugAndroidTestThe jniLibs .sos are build artifacts (gitignored) — CI (and a release
build) run step 1 to produce them. AAPT2 ships x86_64-only, so the Gradle build
must run on a native x86_64 host, and the emulator needs KVM — see
.github/workflows/ci.yml (the android-aar job builds all three ABIs,
assembles the release AAR, and publishes it to a local Maven repo as a dry run).
The device measure seam is wired: pass a MermaidNative.Measurer (use
PaintMeasurer over your drawing Paint) to MermaidNative.scene(source, measurer = …) and native layout measures text with the same face that draws it
— a C trampoline bridges each measure request back into Kotlin on the JNI thread.
MermaidDiagram defaults to the app's Material colors — MermaidTheme.fromMaterial(MaterialTheme.colorScheme)
— so diagrams match the surrounding UI (light/dark included) automatically. Pass
an explicit theme (Compose) or set MermaidView.theme (View) to override.
Under the hood the theme crosses the C ABI as ThemeWire JSON (#RRGGBBAA
colors + prefersDark) to mmk_scene_json_themed, which paints the scene with
those colors instead of a built-in preset.
onNodeClick(nodeId)hit-testing — needs the ABI to emit a node→rect map alongside the scene;SceneWireis flattened primitives with no node identity.- Maven Central push — the AAR builds, bundles all three stripped ABIs, and
publishes to a local repo today (verified: a fresh consumer app resolves
ai.2389:mermaidkit-android:0.1.0and packages the.sointo its APK). Pushing to Central still needs credentials + namespace verification + signing. - Native size — trim the Foundation/ICU surface to shrink the ~64 MB/ABI payload.
