Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ Full module documentation: [hexdocs.pm/mob_dev](https://hexdocs.pm/mob_dev).

## [Unreleased]

### Fixed
- **Android plugin component registration is backward-compatible again.**
`ui_components.android.composable` remains the native registry key used by
existing plugin bridges instead of being treated as a callable Kotlin
symbol. Generated registration is now explicit through `android.factory`;
opted-in factories receive both `props` and the native event sender. The
generated factories run before bridge registration so a bridge-owned
factory remains authoritative for the same key.

## [0.6.31] - 2026-08-31

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion decisions/2026-08-30-android-ui-components-bootstrap.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Android ui_components registration rides the generated MobPluginBootstrap

- Date: 2026-08-30
- Status: accepted
- Status: superseded by
[2026-08-31-explicit-android-component-factories.md](2026-08-31-explicit-android-component-factories.md)

## Context

Expand Down
34 changes: 34 additions & 0 deletions decisions/2026-08-31-explicit-android-component-factories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Android component factory generation is explicit

- Date: 2026-08-31
- Status: accepted

## Context

MobDev 0.6.31 treated `ui_components.android.composable` as a callable Kotlin
symbol. Existing plugins and MobDev's own tier-2 scaffold use that field as the
native-view registry key. Some plugins register their factories from
`android.bridge_class`, including factories that depend on the native event
sender. The generated 0.6.31 registration therefore either failed to compile
or overwrote a working bridge-owned factory with one that discarded events.

## Decision

`android.composable` retains its established registry-key meaning. A component
opts into generated registration with a separate `android.factory` Kotlin
function. The function accepts `(props, send)`; generated code forwards both
arguments. A bare function name is qualified with the package of
`android.bridge_class`, while a fully-qualified name needs no bridge.

Generated factories are installed before bridge `register()` and activity
handoff. A bridge that registers the same key therefore remains authoritative.
Components without `android.factory` receive no generated registration and
continue using their existing bridge- or host-owned path.

## Consequences

- Existing manifests and eventful bridge registrations work unchanged.
- Automatic registration is deliberate and has one event-capable signature.
- Plugins adopting automatic registration may ignore `send` in their Kotlin
function, but the function must accept it.
- MobDev 0.6.31 should be skipped in favor of the containing patch release.
30 changes: 14 additions & 16 deletions lib/mob_dev/native_build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5058,9 +5058,11 @@ defmodule MobDev.NativeBuild do
# Copies each activated plugin's `bridge_kt` into the app's Kotlin sourceSet
# (at its own package path, read from the file's `package` line) so Gradle
# compiles it, and (re)generates `io.mob.plugin.MobPluginBootstrap` whose
# `registerAll(activity)` calls each `bridge_class`'s `register()`, hands
# the Activity to any bridge implementing `MobActivityAware`, and registers
# the plugins' `ui_components` Compose factories with the app's
# `registerAll(activity)` registers opted-in `ui_components` Compose
# factories with the app's MobNativeViewRegistry, then calls each
# `bridge_class`'s `register()` and hands the Activity to any bridge
# implementing `MobActivityAware`. Bridge-owned registrations run last and
# remain authoritative.
# MobNativeViewRegistry (MobDev.Plugin.AndroidBootstrap — the Android
# analog of the iOS mob_register_plugins bootstrap; see mob_scene3d-q03).
# MainActivity calls `MobPluginBootstrap.registerAll(this)` in `onCreate`.
Expand Down Expand Up @@ -5132,11 +5134,9 @@ defmodule MobDev.NativeBuild do
end
end

# Resolves the ui_components half of the bootstrap for the activated
# plugins: classify the manifests (pure), raise on malformed declarations
# (an android-backed component codegen can't register is a manifest bug —
# surface it at build time, next to the manifest, not as a blank view on
# device), and locate the app package that defines MobNativeViewRegistry.
# Resolves explicitly opted-in ui_components factories for the activated
# plugins: classify the manifests, raise on malformed opt-ins, and locate
# the app package that defines MobNativeViewRegistry.
# Hosts without the registry (LiveView wrappers, pre-registry templates)
# can't render native views at all, so declared ui_components get a printed
# warning and no generated registrations there.
Expand All @@ -5150,14 +5150,13 @@ defmodule MobDev.NativeBuild do
)
end

case {classified.registrations ++ classified.placeholders,
__android_app_package__(@android_java_root)} do
case {classified.registrations, __android_app_package__(@android_java_root)} do
{[], _} ->
nil

{_some, nil} ->
IO.puts(
" [plugin android] activated plugins declare ui_components but no " <>
" [plugin android] activated plugins declare android.factory but no " <>
"MobNativeViewRegistry was found under #{@android_java_root} " <>
"(MobBridge.kt) — skipping Compose factory registration. Native " <>
"view components will not render in this host."
Expand Down Expand Up @@ -5264,7 +5263,7 @@ defmodule MobDev.NativeBuild do
ui_body = if ui, do: ui.body, else: ""

calls =
[bridge_calls, ui_call]
[ui_call, bridge_calls]
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n")

Expand Down Expand Up @@ -5295,10 +5294,9 @@ defmodule MobDev.NativeBuild do

"""
// Generated by mob_dev (MobDev.NativeBuild) — do not edit.
// Calls each activated plugin's bridge-class register() at startup, then
// hands the Activity to any bridge implementing MobActivityAware and records
// any bridge implementing MobPermissionProvider; also registers the plugins'
// ui_components Compose factories with the app's MobNativeViewRegistry.
// Registers explicitly opted-in ui_components Compose factories, then calls
// each activated plugin's bridge-class register() and hands the Activity to
// bridges implementing MobActivityAware. Bridge-owned factories run last.
// Invoked from MainActivity.onCreate as registerAll(this), before
// setContent renders anything.
package io.mob.plugin
Expand Down
Loading
Loading