Skip to content

fix(android): apply index after the update's detents - #89

Open
giaBaoJS wants to merge 1 commit into
software-mansion-labs:mainfrom
giaBaoJS:fix/android-index-after-detents
Open

giaBaoJS wants to merge 1 commit into
software-mansion-labs:mainfrom
giaBaoJS:fix/android-index-after-detents

Conversation

@giaBaoJS

Copy link
Copy Markdown
Contributor

On Android, a render that adds a detent and moves index onto it in the same update can leave the sheet where it was. The example app already does this: in "Dynamic detent updates", tap "Shorten detents and select last index" and then "Restore content detent and select it". That second button calls setIndex(2) and brings back the 'content' detent in the same render. On Android the sheet can stay on the middle detent while the screen reports index: 2.

The cause is prop order. Fabric gives a props update to the view manager in the key order of the update's folly::dynamic object, and that is not the order the props were written in JS. BottomSheetViewManager.setIndex hands the value straight to BottomSheetHostView.setIndex, which ignores an index outside the current detents:

if (newIndex >= detentSpecs.size || newIndex == targetIndex) return

So when index comes before detents, index 2 is checked against the old two detents and dropped. The detents setter that runs next keeps the old target. Nothing applies the index again until another update includes index.

I checked the order against folly itself (Homebrew folly 2026.01.12, the same F14NodeMap that backs dynamic::object). I inserted keys the way jsi::dynamicFromValue does, in JS property order:

insert: detents index scrimOpacities
  items(): scrimOpacities index detents      (NDEBUG, same on every run)
insert: detents index
  items(): index detents

In an NDEBUG build, a small update like these comes out of items() in reverse insertion order, so index comes before detents. Without NDEBUG, F14 shuffles the order on purpose, and it changed from run to run, so debug builds hit this only some of the time. getProps in FabricMountingManager.cpp sends newProps->rawProps as is, ReadableNativeMap.importKeys walks items(), and ViewManager.updateProperties calls the setters in that order.

iOS is not affected, because BottomSheetComponentView updateProps: sets detents and then index explicitly. This change gives Android the same order. setIndex now only records the value, and onAfterUpdateTransaction applies it once the whole update has been processed. onAfterUpdateTransaction runs after every props batch, on both the create path and the update path. React Native's own BaseViewManager uses the same hook for transform props, whose setters also depend on each other's order. The pending value is cleared once it is applied, so a later update without index does not apply it again.

Tests: the new BottomSheetViewManagerIndexTest passes updates through ViewManager.updateProperties with an explicit key order, because JavaOnlyMap is a HashMap and happens to put detents first. The first test grows [0, 300] at index 0 to [0, 300, 600] at index 2, with index first and then with detents first, and checks that the target is now an open detent. Without the fix it fails for indexFirst=true. The second test covers the cleanup. After that kind of update, a scrim tap closes the sheet (onIndexChange(0)). A later update that contains only animateContentHeight must not reopen it. BottomSheetViewManagerCloseRequestTest called manager.setIndex directly, outside a props update, so it now sends that one prop through updateProperties. #56 also edits that test file, so whichever lands second will have a one-line conflict there. The Android unit suite goes from 76 to 78 tests, all passing. assembleDebugAndroidTest, lint and typecheck pass.

One behavior change to be aware of: when an update shortens detents and sets an index equal to the clamped target, Android now does what iOS does. The detent refresh clamps the target and animates to it, and the index that follows is a no-op. Before this change, a release build applied index first. By my reading of the code it also emitted an onSettle for that case, and that no longer happens. I have not run this on a device or emulator (none available here). The evidence is the folly ordering above, the code path, and the unit tests.

Fabric hands a props update to the view manager in folly::dynamic map
order, not in the order the props were written. When one render grows
detents and moves index onto a new detent, index can arrive first, fall
outside the old detents and be dropped. Defer index to
onAfterUpdateTransaction so it always sees the update's detents, the
same order iOS applies them in.

This branch has not been deployed

No deployments
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