fix(plugins): register Android ui_components composables from the generated MobPluginBootstrap - #56
Merged
Conversation
…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>
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes mob_scene3d-q03 (the chopaat repro): the plugin manifest's
ui_componentsdeclared the Scene3d viewport composable, but nothing consumed it on Android — every host had to hand-register the Compose factory inMainActivity.onCreate, and a host that forgot rendered the component as nothing (MobNativeViewRegistry.renderreturns silently on an unknown key). iOS already worked via the generatedmob_register_plugins()(MobDev.Plugin.IOSBootstrap); this is the Android analog.Mechanism
New
MobDev.Plugin.AndroidBootstrap(pure classify + Kotlin emit) and auihalf spliced into the generatedio.mob.plugin.MobPluginBootstrap(NativeBuild.__bootstrap_kotlin__/2), whoseregisterAll(this)every generated and adopted MainActivity already calls beforesetContent— so existing hosts get the registrations on their nextmix mob.deploy --nativewith no template or host edit.ui_components.android.view_module, falling back toios.view_module(the key is platform-independent — the Elixir module name with dots → underscores; existing manifests only carry it on the iOS side).ui_components.android.composable: used as-is when fully qualified, otherwise qualified with theandroid.bridge_classpackage (the composable ships in the plugin'sbridge_kt, which declares that package — mob_scene3d:io.mob.scene3d.MobScene3dViewport).MobNativeViewRegistrylives in the app package (MobBridge.kt), which a plugin bridge's ownregister()can never name — but the generated bootstrap can:__android_app_package__/1discovers the package of the file definingobject MobNativeViewRegistryand every reference is emitted fully qualified.Silent-nothing becomes impossible
composable)Mix.raiseat build, next to the manifestandroid.composableswift_structcheck)registerAll: renders a red "Missing native component:<key>(<plugin>)" tile +Log.e. The host's own registration afterregisterAlloverwrites it, so the documented tier-2 flow keeps working — but forgetting can no longer be silentVerification (compile-level)
Generated a fresh
mix mob.new q03hostwith{:mob_scene3d, path: ...}+config :mob, :plugins, [..., :mob_scene3d]and mob_dev pointed at this branch, then ran the realmix mob.deploy --native --androidcodegen path. The generatedMobPluginBootstrap.ktcontains, with no hand edit:…and
./gradlew :app:compileDebugKotlincompiles 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.ktworkaround (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__/2splice/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 HIGHbundled_runtimedrift findings in the machine-level~/.mob/cacheOTP 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 inmix test.ADR:
decisions/2026-08-30-android-ui-components-bootstrap.md.🤖 Generated with Claude Code