Skip to content

fix(plugins): register Android ui_components composables from the generated MobPluginBootstrap - #56

Merged
GenericJam merged 1 commit into
masterfrom
fix/android-ui-components-bootstrap
Aug 31, 2026
Merged

fix(plugins): register Android ui_components composables from the generated MobPluginBootstrap#56
GenericJam merged 1 commit into
masterfrom
fix/android-ui-components-bootstrap

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Fixes mob_scene3d-q03 (the chopaat repro): the plugin manifest's ui_components declared the Scene3d viewport composable, but nothing consumed it on Android — every host had to hand-register the Compose factory in MainActivity.onCreate, and a host that forgot rendered the component as nothing (MobNativeViewRegistry.render returns silently on an unknown key). iOS already worked via the generated mob_register_plugins() (MobDev.Plugin.IOSBootstrap); this is the Android analog.

Mechanism

New MobDev.Plugin.AndroidBootstrap (pure classify + Kotlin emit) and a ui half spliced into the generated io.mob.plugin.MobPluginBootstrap (NativeBuild.__bootstrap_kotlin__/2), whose registerAll(this) every generated and adopted MainActivity already calls before setContent — so existing hosts get the registrations on their next mix mob.deploy --native with no template or host edit.

  • Registry key = ui_components.android.view_module, falling back to ios.view_module (the key is platform-independent — the Elixir module name with dots → underscores; existing manifests only carry it on the iOS side).
  • Composable = ui_components.android.composable: used as-is when fully qualified, otherwise qualified with the android.bridge_class package (the composable ships in the plugin's bridge_kt, which declares that package — mob_scene3d: io.mob.scene3d.MobScene3dViewport).
  • App package problem dissolved by codegen: MobNativeViewRegistry lives in the app package (MobBridge.kt), which a plugin bridge's own register() can never name — but the generated bootstrap can: __android_app_package__/1 discovers the package of the file defining object MobNativeViewRegistry and every reference is emitted fully qualified.

Silent-nothing becomes impossible

Failure Where it gets loud
Typo'd / missing composable symbol Gradle Kotlin compile error (the generated call references it)
Malformed declaration (no registry key, or no composable) Mix.raise at build, next to the manifest
Non-identifier android.composable Validator error at validate time (mirrors the swift_struct check)
Declared but unresolvable (bare composable, no bridge — the hand-copied tier-2 workflow) Generated placeholder registered at registerAll: renders a red "Missing native component: <key> (<plugin>)" tile + Log.e. The host's own registration after registerAll overwrites it, so the documented tier-2 flow keeps working — but forgetting can no longer be silent
Host without the registry (LiveView wrapper / pre-registry template) Printed build warning; UI codegen skipped (no native-view render path exists there at all)

Verification (compile-level)

Generated a fresh mix mob.new q03host with {:mob_scene3d, path: ...} + config :mob, :plugins, [..., :mob_scene3d] and mob_dev pointed at this branch, then ran the real mix mob.deploy --native --android codegen path. The generated MobPluginBootstrap.kt contains, with no hand edit:

private fun registerUiComponents() {
    // mob_scene3d: Mob_Scene3d_Viewport
    com.example.q03host.MobNativeViewRegistry.register("Mob_Scene3d_Viewport") { props, _send ->
        io.mob.scene3d.MobScene3dViewport(props)
    }
}

…and ./gradlew :app:compileDebugKotlin compiles it green (registration + placeholder variants). Device run not performed (build pool hardware is leased; deploy was pointed at a nonexistent device id on purpose) — the chopaat device evidence for the identical hand-written registration (bead repro) covers the runtime half.

This makes the chopaat MainActivity.kt workaround (tagged mob_scene3d-q03) removable — tracked as chopaat-df3.

Gates

  • mix format --check-formatted
  • mix credo --strict ✓ (no issues, 311 files)
  • mix compile --warnings-as-errors
  • mix test ✓ (2180 passed; new: AndroidBootstrap unit suite, __bootstrap_kotlin__/2 splice/order/byte-stability, __android_app_package__ discovery, validator composable accept/reject)
  • mix erlfmt --check priv/android/crypto.erl
  • mix mob.security_scan --strict: 6 pre-existing HIGH bundled_runtime drift findings in the machine-level ~/.mob/cache OTP tarballs (manifest-vs-binary drift, present on master too, unrelated to this diff); all code layers clean. CI runs the non-strict scan.

No priv/ compile-time resources added (codegen is inline source), so no packed-artifact surface change; the existing packed-artifact regression still passes in mix test.

ADR: decisions/2026-08-30-android-ui-components-bootstrap.md.

🤖 Generated with Claude Code

…erated bootstrap

The manifest's ui_components.android entry was data nobody consumed on
Android: every host hand-registered each plugin's Compose factory in
MainActivity.onCreate, and a host that forgot rendered the component as
nothing — MobNativeViewRegistry.render returns silently on an unknown
key (mob_scene3d-q03). iOS already worked via the generated
mob_register_plugins() (MobDev.Plugin.IOSBootstrap).

MobDev.Plugin.AndroidBootstrap now classifies the activated plugins'
android-backed ui_components and NativeBuild splices the registrations
into the generated io.mob.plugin.MobPluginBootstrap, whose
registerAll(this) every generated/adopted MainActivity already calls
before setContent. The registry key is android.view_module falling back
to ios.view_module; a bare composable is qualified with the
bridge_class package; the app package (where MobNativeViewRegistry
lives) is discovered from the file that defines it and referenced fully
qualified — the reason a plugin bridge's own register() never could.

Silent-nothing becomes impossible: a typo'd composable fails the Gradle
Kotlin compile, a malformed declaration (no key / no composable) raises
at build next to the manifest, a declared-but-unresolvable composable
(hand-copied tier-2 workflow) registers a loud red placeholder tile +
Log.e that the host's own later registration overwrites, and the
validator rejects composables that aren't Kotlin identifiers or dotted
paths at validate time instead of Gradle time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@GenericJam

Copy link
Copy Markdown
Owner Author

Reviewed: right layer (the deploy-time bootstrap codegen seam where iOS's working analog lives — mob_new correctly untouched), loud-failure design verified at every tier including the proven Gradle failure on a typo'd composable, the manual tier-2 flow preserved via the overwritable placeholder, and the registry-less LiveView-host case handled with a warning instead of broken codegen. Diff sweep clean; CI green (2180 tests + security workflows); chopaat's identical hand-written registration provides the runtime-half evidence. Merging and cutting 0.6.31 — the chopaat workaround removal is tracked (chopaat-df3).

@GenericJam
GenericJam merged commit b2375eb into master Aug 31, 2026
3 checks passed
@GenericJam
GenericJam deleted the fix/android-ui-components-bootstrap branch August 31, 2026 18:52
GenericJam added a commit that referenced this pull request Sep 1, 2026
…erated bootstrap (#56)

The manifest's ui_components.android entry was data nobody consumed on
Android: every host hand-registered each plugin's Compose factory in
MainActivity.onCreate, and a host that forgot rendered the component as
nothing — MobNativeViewRegistry.render returns silently on an unknown
key (mob_scene3d-q03). iOS already worked via the generated
mob_register_plugins() (MobDev.Plugin.IOSBootstrap).

MobDev.Plugin.AndroidBootstrap now classifies the activated plugins'
android-backed ui_components and NativeBuild splices the registrations
into the generated io.mob.plugin.MobPluginBootstrap, whose
registerAll(this) every generated/adopted MainActivity already calls
before setContent. The registry key is android.view_module falling back
to ios.view_module; a bare composable is qualified with the
bridge_class package; the app package (where MobNativeViewRegistry
lives) is discovered from the file that defines it and referenced fully
qualified — the reason a plugin bridge's own register() never could.

Silent-nothing becomes impossible: a typo'd composable fails the Gradle
Kotlin compile, a malformed declaration (no key / no composable) raises
at build next to the manifest, a declared-but-unresolvable composable
(hand-copied tier-2 workflow) registers a loud red placeholder tile +
Log.e that the host's own later registration overwrites, and the
validator rejects composables that aren't Kotlin identifiers or dotted
paths at validate time instead of Gradle time.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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