fix(android): give ComposeView its own Lifecycle to fix #1103 and #1104 - #1105
Conversation
…and callstack#1104 react-native-screens fully removes and re-adds a covered screen's Fragment instead of just hiding it, destroying its view-tree Lifecycle. Compose's default disposal strategy keys off that ambient Lifecycle, so the pager's ComposeView was torn down and rebuilt on every cover/reveal cycle, causing a blank pager (callstack#1103) and loss of page state such as FlatList scroll position (callstack#1104). Giving the ComposeView its own self-owned Lifecycle lets the composition survive the cycle untouched.
There was a problem hiding this comment.
Pull request overview
This PR fixes Android regressions in the Compose-based pager when screens are covered/revealed (notably with react-native-screens) by decoupling the ComposeView’s composition disposal from the ambient Fragment view-tree Lifecycle, keeping the composition (and hosted native page views) alive across cover/reveal cycles.
Changes:
- Add explicit cleanup in
onDropViewInstanceto destroy a self-ownedLifecycleand dispose the Compose composition. - Rework
ComposePagerViewto create/bind aComposeViewwith a dedicatedLifecycleOwnerandDisposeOnLifecycleDestroyedstrategy. - Add an AndroidX Lifecycle runtime dependency required for view-tree lifecycle owner APIs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt | Disposes pager resources when the RN view instance is dropped. |
| android/src/main/java/com/reactnativepagerview/ComposePagerView.kt | Introduces a self-owned lifecycle for the ComposeView and adjusts attach/detach behavior to preserve composition across cover/reveal. |
| android/build.gradle | Adds AndroidX Lifecycle runtime dependency for the new lifecycle owner usage. |
Suppressed comments (1)
android/src/main/java/com/reactnativepagerview/ComposePagerView.kt:652
- ComposeViewLifecycleOwner is hard-coded to start in Lifecycle.State.RESUMED and stay there until DESTROYED. That can keep lifecycle-aware work (e.g., coroutines/flows started with lifecycle-aware APIs) running even while the view is detached/covered, which is likely more work than necessary. Consider starting at CREATED and adding explicit attach/detach state transitions so the composition can pause while offscreen without being disposed.
// A Lifecycle that isn't derived from the ambient Fragment/Activity: it
// starts RESUMED and only moves to DESTROYED when destroy() is called
// explicitly, so it survives being covered/revealed by react-native-screens'
// Fragment remove+re-add (see the composeLifecycleOwner field comment on
// ComposePagerView above).
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| override fun onDetachedFromWindow() { | ||
| updateSameOrientationAncestorsGestureState(false) | ||
| if (composeView.parent === this) { | ||
| super.removeView(composeView) | ||
| didSetContent = false | ||
| } | ||
| // composeView is intentionally left attached and alive here: its | ||
| // Lifecycle is self-owned (see composeLifecycleOwner) and only reaches | ||
| // DESTROYED in dispose(), so there is nothing to tear down on a mere |
| // Needed for ComposeViewLifecycleOwner (see ComposePagerView.kt), which | ||
| // gives the ComposeView a self-owned Lifecycle instead of the ambient one. | ||
| implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" |
|
@GaelCO If ComposeView uses that lifecycle directly, its composition is disposed or becomes unusable, resulting in the blank pager reported in #1103. Recreating the entire ComposeView fixes the blank state, but also recreates every page’s native host, causing state such as FlatList scroll position to be lost (#1104). The fix gives ComposeView a stable, pager-owned lifecycle. It:
This is also an improvement over the previous implementation, which treated every attached view as RESUMED. A view can remain attached while its Activity or Fragment is paused or stopped, so lifecycle-aware content could otherwise keep running in the background. I ran the new ComposeViewLifecycleOwnerTest suite successfully. Before After |
Summary
ComposeViewwas torn down and rebuilt on every cover/reveal cycle: this caused a blank pager after covering the screen ([Android] Tab content stays blank after navigating away and back over a PagerView (regression 8.0.5 → 9.0.2) #1103) and reset each page's native view state, e.g. FlatList scroll position ([Android] PagerView 9 remounts/recreates page after navigating back to Material Top Tabs #1104).ComposeViewits own self-ownedLifecycle(only destroyed for real inonDropViewInstance) lets the composition and each page's host survive the cover/reveal cycle untouched.Fixes #1103
Fixes #1104
Test plan
react-navigationmaterial-top-tabs stack that covering/revealing a screen containing the pager no longer blanks it out.