Skip to content

feat(android): Kotlin/Jetpack Compose backend - #135

Merged
tiensonqin merged 7 commits into
mainfrom
devin/android-kotlin
Oct 6, 2026
Merged

tiensonqin merged 7 commits into
mainfrom
devin/android-kotlin

Conversation

@tiensonqin

@tiensonqin tiensonqin commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a Kotlin / Jetpack Compose backend that renders the LUI wire protocol on Android — a full port of the Apple backend's coverage, plus the KotlinHost wiring needed for the OCaml side to target it.

Note on diff size: this branch is built on top of devin/remove-flutter (per coordinator sequencing); the Flutter-deletion diff shows here until #134 lands.

Kotlin library (platform/android/lui, package dev.lui, ~4.9k lines):

  • LuiWire — patch-batch JSON parser (kotlinx.serialization) covering all ops
  • LuiStore / LuiRetainedTree — retained node store; atomic apply (whole batch rejected on any op failure, generation must advance by 1, root validated for exactly one child post-apply)
  • LuiNodeView / LuiRender / LuiKindRules — per-kind Compose renderers + property validation matrix
  • LuiTheme — semantic token mapping (glass, surface-container-high, muted/secondary/accent, spacing/radii/typography) onto a Material3 colorScheme, tuned for both light and dark
  • LuiExtension — extension registry with composable builders + event emit; LuiSplit — split-view/branch/pane/tab port (draggable ratio divider emitting ratio-changed, tab bar emitting tab-selected/tab-closed/pane-focused)
  • LuiBridge + lui_jni_bridge.c — JNI bridge under the lui_kotlin_* named-value prefix; same lock discipline as the C bridge (patch callback runs under the OCaml domain lock: copies the JSON out and dispatches UI-side)

Codegen: renderKotlin emitter in tooling/generate_component_schema.mjs → LuiWireSchema.kt (kind/property enums + support matrices). Reuses the swift name field; the one collision (name → propName, Enum.name) is handled in the generator.

Host wiring: KotlinHost in host_kind (protocol + host_code 4), kotlin host name, {android,kotlin} in the all/splits extension profiles, native-card moved to a new web-and-kotlin profile, fingerprint literals recomputed across all split host sources + tests, lui_element_combine mobile branches restored keyed on KotlinHost. components_bridge.ml registers lui_kotlin and gains the missing extension_event entry point (shared gap — the split/markdown/navigation bridges already had it).

Gallery app (examples/components/android, appId dev.lui.components, matching the Maestro flow): loads liblui_components.so, registers split + gallery extensions, renders the tree. CMake falls back to a stub when the OCaml .so is absent.

Screenshots (Paparazzi goldens, phone size)

Light Dark
gallery light gallery dark
controls light controls dark

Kind coverage

All 88 wire kinds accepted; rendered: 86. No-op (documented, mirrors other backends):

  • filePreview — platform-native file preview has no generic Compose equivalent
  • br — semantic line break, layout is handled by text kinds

Modal surfaces (dialog/sheet/popover) render via LuiModalSurface; remaining kinds render through dedicated per-kind composables or the chrome-surface + children path.

Verification

  • dune runtest -j 4: 66/66 green (incl. fingerprint drift check now covering LuiSplit.kt)
  • node --test tooling/test/*.mjs: 6/6
  • gradle :lui:testDebugUnitTest: 18 JVM wire/store tests green
  • gradle :lui:verifyPaparazziDebug: 4 goldens green (light + dark)
  • gradle -p examples/components/android assembleDebug: gallery APK builds

Stubbed / gaps

  • liblui_components.so can't be built on this VM (lg Android OCaml toolchain absent); CMake compiles lui_jni_bridge_stub.c so the app still builds — run tooling/mobile/build_components_android.sh on a machine with the toolchain for the real runtime
  • Split pane mounts only the selected tab (matches web); drag-and-drop tab re-order events are declared but drop-target UI is minimal
  • compileSdk pinned to 35 — Paparazzi 1.3.5 breaks on 36 (android.os.Build reflection); bump when Paparazzi supports it

For the chat-Android session

The dev.lui public surface is aligned with the module vendored in logseq/chat#37 — the includeBuild "../../lui/platform/android" swap is a pure delete:

  • Gradle module: project(":lui") at platform/android/lui (composite include via includeBuild)
  • LuiBackend(onEvent: (LuiEvent) -> Unit, extensions: LuiExtensionRegistry, icons: LuiIconResolver); applyJson(raw: String?): Boolean, Content(rootId: Long = -1L) (all roots when negative), node(id: Long)
  • LuiEvent sealed class, node: Long ids everywhere; LuiEvent.Extension(node, identifier, name, values: Map<String, Any?>)
  • LuiIconResolver fun interface { icon(name): ImageVector? } + or() + DEFAULT — app:-prefixed names fall through to the app-level resolver
  • LocalLuiThemeDark (Boolean? — null follows the system), LuiTheme(dark, semanticColors, ...) { ... }
  • Extensions: registry.register(id, fingerprint) { context -> ... }, context.string/int/double/flag/property<T>, context.emit(name, Map<String, Any?>), context.Children()
  • LuiBridge.start(libraryName) { patchJson -> backend.applyJson(patchJson) } — optional; calling NativeCore.lui* directly works identically

Link to Devin session: https://app.devin.ai/sessions/78ea579791324d83bc9dc95abc89fdfe
Open in Devin Desktop: https://app.devin.ai/desktop/session/78ea579791324d83bc9dc95abc89fdfe?variant=devin
Requested by: @tiensonqin

Delete platform/flutter and the Flutter gallery app, drop FlutterHost
from the protocol and extension registries, strip dart codegen from
the component-schema and extension-API generators, and repoint the
mobile build scripts at the upcoming examples/components/android app.

All flutter/flutter-profile fingerprints and docs references are
removed; rg -il flutter returns zero hits.
Wire-schema codegen (renderKotlin) emits LuiWireSchema.kt; the dev.lui
library carries the wire parser, retained store + validation, per-kind
Compose renderers, Material-3 theme token mapping, JNI bridge
(lui_jni_bridge.c + LuiBridge), and the gallery app shell at
examples/components/android (appId dev.lui.components).

Screenshot tests and the KotlinHost wiring still follow.
… fixes

- JUnit coverage for wire parse / store semantics / extension registry
- Paparazzi snapshots (PIXEL_5, light+dark) feeding patch JSON through applyBatch
- compileSdk/targetSdk 35: Paparazzi 1.3.5 breaks on android-36's Build layout
- settings.gradle :lui projectDir path fix; vector launcher icon
- switch/checkbox inline layout; button labels ellipsize instead of wrapping
- AutoMirrored icon deprecations
…/android-kotlin

# Conflicts:
#	tooling/generate_component_schema.mjs
…backs

- host_kind gains KotlinHost (protocol, bridge code 4, 'kotlin' host name)
- extension_schemas.json: {android,kotlin} in all/splits profiles;
  native-card moves to new web-and-kotlin profile
- fingerprint literals recomputed across split host sources and tests
- lui_element_combine: restore mobile-host branches keyed on KotlinHost
- LuiSplit.kt: split-view/branch/pane/tab Compose extensions with
  fingerprint literals, added to split_host_sources drift check
- components_bridge.ml: register "lui_kotlin" callback prefix and the
  missing extension_event entry point (shared gap; split/markdown
  bridges already had it)
- gallery app registers split components + native-card + gallery-accent
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

…kend

Node ids and wire-op id fields are Long throughout. LuiBackend now takes
(onEvent, extensions, icons) constructor args with applyJson(String?),
Content(rootId: Long), and node(id). LuiIcons becomes the LuiIconResolver
fun interface with .or() chaining and a DEFAULT instance, so app-owned
(app:-prefixed) icon names fall through to the app-level resolver.
LuiExtension gains the minimal register(id, fingerprint) { context ->
... } form plus flag/property<T>/emit(name, Map)/Children() helpers, and
extension events carry Map<String, Any?>. LocalLuiThemeDark (Boolean?)
lets hosts override dark mode; null falls back to the system setting.

This keeps the public surface identical to the dev.lui module logseq/chat
vendored in its Android app so switching to includeBuild is a pure delete.
@tiensonqin
tiensonqin merged commit afeb4cc into main Oct 6, 2026
4 checks passed
@tiensonqin
tiensonqin deleted the devin/android-kotlin branch October 6, 2026 13:47
devin-ai-integration Bot pushed a commit to logseq/chat that referenced this pull request Oct 6, 2026
tiensonqin added a commit to logseq/chat that referenced this pull request Oct 7, 2026
- Bump the lui pin to 264aa9f67da0d0d3e413f9dfe970785e7286f23c
  (logseq/lui#135; re-pin to the merge SHA once that PR lands).
- Rename every FlutterHost reference in shared/ to KotlinHost and
  android/flutter -> android/kotlin in extension profiles, fingerprints,
  and all flutter_* identifiers/comments; host code 4 now maps to
  KotlinHost (matching lui's components_bridge.ml table: 1 Web,
  2 SwiftUI, 4 Kotlin, 6 GPUI), so iOS passes host code 2 and Android 4.
- shared/: bare 'reactive' calls rewritten to Signal.map — the new
  lui.ppx expands bare reactive to dyn in element position while
  ~p:(reactive ..) labeled forms are kept for prop signals.
- Delete the vendored android/lui module; :lui now resolves to
  ../lui/platform/android/lui via module include, the same consumption
  model as lui's examples/components/android.
- LuiDispatch handles the widened LuiEvent sealed class (pointer,
  context-menu, load events have no chat FFI export and are dropped,
  matching upstream dispatchToBridge).
- Disable :lui test tasks in this build; the backend's paparazzi
  tests need its standalone AGP 8.6.1 build.

Verification: dune build @shared/native/runtest (719 tests),
./gradlew assembleDebug assembleProfile test all green against the
real backend; profile APK runs on the chat_test emulator with a fresh
x86_64 liblogseq_chat_core.so and renders the auth screen.
tiensonqin added a commit to logseq/chat that referenced this pull request Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant