Generate the Android Sheet renderer (Material 3 ModalBottomSheet) - #36
Merged
Conversation
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>
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.
Summary
Mob.UI.sheet/2native modal Sheet primitive (MOB-101).MobNode.ktneeded zero changes — parsing is fully generic, no per-type allowlist anywhere.MobBridge.kt:"sheet"dispatch →MobSheet(node), Material 3ModalBottomSheetwith detent logic (medium-only rejectsExpandedviaconfirmValueChange), custom drag indicator, exactly-once dismissal via a remembered per-presentation flag.PartiallyExpandedanchor 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 allowsExpanded.ModalBottomSheetownscontainerColor/shapeas constructor params (same reasonButtonalready readsbackground/corner_radiusdirectly instead of viaModifier— they don't compose through a modifier chain). The"sheet"dispatch arm deliberately does not pass thenodeModifier(node.props)-derivedmthrough;MobSheetbuilds its content modifier fromnode.props - listOf("background", "corner_radius"). New structural lintMobNew.Templates.Lint.sheet_content_modifier_not_double_applied/1guards both halves against regression (neither is something a compiler catches).androidTestinstrumentation scaffolding — first added to this template (build.gradle.eexhad zero test deps before this: Compose UI test + Espresso +ui-test-manifest) — plusMediumOnlySheetTest.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(withMOB_DIR/MOB_DEV_DIRset per this repo's worktree convention) — 335 passed, including newdescribe "sheet_content_modifier_not_double_applied/1"inlint_test.exsand aproject_generator_test.exspin asserting the dispatch arm, imports, detent logic, the double-application guard, and exactly-once dismissal are all present in generated outputmix format/mix credo --strict/mix test --only lint(generate + ktlint) — all cleanMediumOnlySheetTestviaadb shell am instrumenton a real emulator — 1/1 pass. Full main APK also builds and installs cleanly.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)