Skip to content

Generate the Android Sheet renderer (Material 3 ModalBottomSheet) - #36

Merged
GenericJam merged 2 commits into
masterfrom
feat/mob-sheet-native-primitive
Aug 27, 2026
Merged

Generate the Android Sheet renderer (Material 3 ModalBottomSheet)#36
GenericJam merged 2 commits into
masterfrom
feat/mob-sheet-native-primitive

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Summary

  • Companion to mob#86 — generates the Android side of the new Mob.UI.sheet/2 native modal Sheet primitive (MOB-101).
  • MobNode.kt needed zero changes — parsing is fully generic, no per-type allowlist anywhere.
  • MobBridge.kt: "sheet" dispatch → MobSheet(node), Material 3 ModalBottomSheet with detent logic (medium-only rejects Expanded via confirmValueChange), custom drag indicator, exactly-once dismissal via a remembered per-presentation flag.
  • Medium-only short-content fix: Material 3 can omit the PartiallyExpanded anchor when content is shorter than half the viewport, which would leave a medium-only sheet with no valid anchor and stuck hidden. BoxWithConstraints + heightIn(min = maxHeight * 0.5f + 1.dp) forces just enough height when (and only when) mediumOnly — never allows Expanded.
  • Double-application fix: ModalBottomSheet owns containerColor/shape as constructor params (same reason Button already reads background/corner_radius directly instead of via Modifier — they don't compose through a modifier chain). The "sheet" dispatch arm deliberately does not pass the nodeModifier(node.props)-derived m through; MobSheet builds its content modifier from node.props - listOf("background", "corner_radius"). New structural lint MobNew.Templates.Lint.sheet_content_modifier_not_double_applied/1 guards both halves against regression (neither is something a compiler catches).
  • New androidTest instrumentation scaffolding — first added to this template (build.gradle.eex had zero test deps before this: Compose UI test + Espresso + ui-test-manifest) — plus MediumOnlySheetTest.
  • Also fixes an orphaned MOB-100 gap: MobNativeViewRegistry.render()'s -1-sentinel guard was written but never shipped in an earlier session — released separately as mob_new 0.4.23, unrelated to this PR's diff.

Test plan

  • mix test (with MOB_DIR/MOB_DEV_DIR set per this repo's worktree convention) — 335 passed, including new describe "sheet_content_modifier_not_double_applied/1" in lint_test.exs and a project_generator_test.exs pin asserting the dispatch arm, imports, detent logic, the double-application guard, and exactly-once dismissal are all present in generated output
  • mix format / mix credo --strict / mix test --only lint (generate + ktlint) — all clean
  • Real device verification: generated a fresh project, built it with Gradle against a real Android SDK, ran MediumOnlySheetTest via adb shell am instrument on a real emulator — 1/1 pass. Full main APK also builds and installs cleanly.
  • Confirmed the fix follows the exact spec'd approach; a negative-control check (temporarily disabling the fix) did not reproduce a clean failure on this specific emulator/Compose-BOM combination — noted honestly in decisions/2026-08-26-native-sheet-primitive.md (mob repo) as a verification limitation worth another look on a different device/BOM, not swept under the rug.

Linear: MOB-101 (cross-repo — see mob#86 for the Elixir + iOS side)

GenericJam and others added 2 commits August 26, 2026 18:22
Companion to the mob repo's Mob.UI.sheet/2 (MOB-101). MobNode.kt
needed zero changes — parsing is fully generic, no per-type allowlist.
MobBridge.kt gets:

- "sheet" dispatch -> MobSheet(node), deliberately NOT threading the
  nodeModifier(node.props)-derived `m` through, since ModalBottomSheet
  owns its own containerColor/shape the same way Button already does
  (via constructor params, not a Modifier chain) -- passing `m` would
  double-apply background/corner_radius.
- Detent logic: medium-only sheets reject Expanded via
  confirmValueChange. Material 3 can omit the PartiallyExpanded anchor
  when content is shorter than half the viewport, which would leave a
  medium-only sheet with no valid anchor and stuck hidden -- fixed by
  forcing content to slightly over half the measured viewport height
  in that case only (BoxWithConstraints + heightIn), never allowing
  Expanded.
- Exactly-once dismissal via a remembered per-presentation flag.
- New structural lint (sheet_content_modifier_not_double_applied/1)
  guarding both halves of the double-application fix -- the dispatch
  arm not passing `m`, and MobSheet actually stripping
  background/corner_radius before building its content modifier.
  Neither half is something a compiler catches.

New androidTest instrumentation scaffolding (first in this template --
build.gradle.eex had zero test deps before this) + MediumOnlySheetTest,
covering the medium-only short-content fix. Confirmed 1/1 pass on a
real emulator against a freshly generated project.

MOB-101

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
detentsProp() was `(node.props["detents"] as? List<*>)` — MobNode's
parser hands JSON array props through as org.json.JSONArray, not
Kotlin List, so the cast silently failed on every real BEAM-sent tree
and fell through to the hardcoded ["medium", "large"] default. The
medium-only detent feature — this PR's headline addition — could
never actually trigger from production data, only from a hand-built
MobNode in a test. Follows the same JSONArray-branch pattern already
proven correct for :tabs (tabDefsProp).

MediumOnlySheetTest now builds its MobNode via JSONObject(...).toMobNode()
(the real parsing path) instead of a hand-built Kotlin listOf(...), so
it exercises the code path the bug actually lived in. Re-verified 1/1
pass on a real Pixel emulator against a freshly generated project.

Also: dedupe function_span/2 and mob_bridge_span/1's shared
find-anchor-then-matching-brace logic into span_after_anchor/2.
Verified dismissSent is not redundant with `visible` (a second
onDismissRequest firing before recomposition removes the sheet isn't
gated by visible alone) — no change made there.

Not merged/released — awaiting review per standing instruction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@GenericJam
GenericJam merged commit 8f57232 into master Aug 27, 2026
3 checks passed
@GenericJam
GenericJam deleted the feat/mob-sheet-native-primitive branch August 27, 2026 04:16
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