Bump to 0.4.24 — Android Sheet renderer + dismissal wire-shape fix (MOB-104) - #38
Conversation
sendDismissOnce() routed through MobBridge.nativeSendTap, which sends
{:tap, tag}. But Mob.UI.sheet/2 documents :on_dismiss as {:dismiss, tag} and
iOS delivers exactly that, so a screen written to the contract never matched:
Mob.Screen forwards the unmatched message to handle_info, which raises
FunctionClauseError and kills the screen process — or silently drops it with a
catch-all, in which case the BEAM never learns the sheet closed while MobSheet's
remembered `visible` is already false, so the sheet is invisible AND
unre-presentable from that slot.
Android had no dismiss sender at all, so this needs the paired mob change
(mob_send_dismiss + mob_beam.h declaration). Adds here:
- nativeSendDismiss extern on MobBridge. It must stay declared on MobBridge —
the JNI symbol is Java_<pkg>_MobBridge_nativeSendDismiss and resolution is by
DECLARING class, not call site. That is the MOB-98 failure mode.
- The matching beam_jni.c thunk calling mob_send_dismiss.
- sendDismissOnce switched over.
Test changes worth calling out: project_generator_test.exs asserted the buggy
shape verbatim ("dismissHandle?.let { MobBridge.nativeSendTap(it) }"), so it was
pinning the bug green — a correct fix would have failed the suite. Now asserts
nativeSendDismiss, refutes the tap form, and covers both halves of the pairing
(the MobBridge extern and the beam_jni thunk), matching how orientation and
connectivity are already covered.
The lockstep is compile-enforced: the "beam_jni.c passes clang -fsyntax-only"
test builds the rendered thunk against MOB_DIR's mob_beam.h, so this suite fails
against a mob without mob_send_dismiss. Verified 335/335 against a mob checkout
carrying the pair. mob must be released first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the adversarial review of the MOB-104 branch.
- Version bump + CHANGELOG. The whole Android Sheet renderer was sitting
unreleased on master at the already-published 0.4.23, so published mob_new
generates apps with NO Android sheet while published mob ships
Mob.UI.sheet/2. The entry names the wire-shape change explicitly and carries
an Upgrading section: MobBridge.kt and beam_jni.c are app-owned and never
re-rendered, so existing projects need a hand-port, and anyone who worked
around the old bug by matching {:tap, tag} now has a dead clause.
- Generated dep floor raised to {:mob, "~> 0.7.31"} (was "~> 0.7"). The
generated beam_jni.c calls mob_send_dismiss, which mob only exports from
0.7.31; the looser constraint resolved an older mob happily and then failed
late in `mix mob.deploy --android --native` with "call to undeclared
function 'mob_send_dismiss'" and nothing pointing at the real fix.
- Corrected a misleading test comment claiming the nativeSendDismiss presence
assertion guards the MOB-98 declaring-class failure mode. It does not — a
substring match still passes if the declaration moves to another object.
Ownership is pinned by Lint.native_funs_owned_by_mob_bridge/1 via its own
generator test, which covers this fun automatically; the comment now says so,
so nobody weakens that test believing this one covers it.
- MobBridge class doc pointed at mob_nif.c, which no longer exists (it is
mob_nif.zig), and described only the tap route. Now also documents
nativeSendDismiss -> mob_send_dismiss -> {:dismiss, tag} and why it
deliberately doesn't reuse the tap sender.
335 tests (with MOB_DIR pointed at the paired mob), format, credo --strict,
and the ktlint generate-then-lint check all clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Device-verified on a physical Moto G power (2021)The one thing the adversarial review couldn't check from source — that the JNI symbol resolves at runtime and Built a probe app from these templates (
Two dismissals produced exactly two entries, so Full chain confirmed end to end: Unrelated bug found on the wayA freshly generated app can't complete its first Android build: the manifest declares |
Unblocks the downstream Android Sheet work. Held from release until now because the headline feature's dismiss contract was broken.
The bug
sendDismissOnce()routed throughMobBridge.nativeSendTap, delivering{:tap, tag}. ButMob.UI.sheet/2documents:on_dismissas{:dismiss, tag}and iOS delivers exactly that. A screen written to the documented contract never matched:Mob.Screenforwards the unmatched message tohandle_info, raisingFunctionClauseErrorand killing the screen — or a catch-all swallowed it, leaving the BEAM unaware the sheet closed whileMobSheet's rememberedvisiblewas alreadyfalse, so the sheet was invisible and unre-presentable.Android had no dismiss sender at all, so this needs the paired
mobchange (mob_send_dismiss, GenericJam/mob#90).What's here
nativeSendDismissextern onMobBridge+ the matchingbeam_jni.cthunk;sendDismissOnceswitched over.mob_newgenerates apps with no Android sheet while publishedmobshipsMob.UI.sheet/2.{:mob, "~> 0.7.31"}(was"~> 0.7"). The generatedbeam_jni.ccallsmob_send_dismiss; the looser constraint resolved an older mob happily and then failed late inmix mob.deploy --android --native.### Upgradingnote:MobBridge.kt/beam_jni.care app-owned and never re-rendered, so existing projects need a hand-port.mix mob.doctornow warns (MOB-104: mix mob.doctor detects the stale sheet-dismissal wire shape mob_dev#48).Test changes worth reading
project_generator_test.exs:791asserted the buggy string verbatim, so it was pinning the bug green — a correct fix would have failed the suite. It now assertsnativeSendDismiss, refutes the tap form, and covers both halves of the pairing.An adversarial review also caught a comment of mine claiming the presence assertion guards the MOB-98 declaring-class failure mode. It doesn't — a substring match still passes if the declaration moves. Ownership is pinned by
Lint.native_funs_owned_by_mob_bridge/1via its own test; the comment now says so, so nobody weakens that test believing this one covers it.Release ordering — hard constraint
mob0.7.31 must be released first. Thebeam_jni.c passes clang -fsyntax-onlytest compiles the rendered thunk againstMOB_DIR'smob_beam.h, so this suite fails against a mob withoutmob_send_dismiss— which is exactly what the pre-push hook hit here. 335/335 pass against a mob carrying the pair.Also clean:
mix format,mix credo --strict, and the ktlint generate-then-lint check.🤖 Generated with Claude Code