Skip to content

Expose onRouteLifecycleStateChanged(Lifecycle.State) API to allow ActivityStoreContext.isRouteStarted() and ActivityStoreContext.isRouteResumed() keep functioning under non-Fragment navigation host (Compose Nav 3).#515

Closed
alexanderbezverhni wants to merge 3 commits into
masterfrom
bezverhni/fragment_lifecycle

Conversation

@alexanderbezverhni

Copy link
Copy Markdown
Collaborator

No description provided.

…ivityStoreContext.isRouteStarted() and ActivityStoreContext.isRouteResumed() keep functioning under non-Fragment navigation host (Compose Nav 3).
@alexanderbezverhni alexanderbezverhni self-assigned this Jun 3, 2026

@olive-agent olive-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formula-android/src/main/java/com/instacart/formula/android/internal/ActivityManager.kt:77

ActivityStoreContextImpl now keeps route lifecycle state until a route reports DESTROYED, but ActivityManager is reused across configuration changes via AppManager. In the non-Fragment host path, there is no automatic equivalent of NavigationFlowRenderView.onFragmentViewDestroyed(), so on activity recreation this delegate can retain stale STARTED/RESUMED entries from the previous activity instance. That means isRouteStarted() / isRouteResumed() can replay true for routes that no longer exist until the new host explicitly re-drives every route. Should we clear the stored route lifecycle state here on activity destroy, or otherwise prove recreation is handled correctly for the new API?

@carrotkite

carrotkite commented Jun 3, 2026

Copy link
Copy Markdown

JaCoCo Code Coverage 93.83% ✅

Class Covered Meta Status
com/instacart/formula/android/NavigationStore 83% 0%
com/instacart/formula/android/internal/ActivityManager 50% 0%
com/instacart/formula/android/internal/ActivityStoreContextImpl 100% 0%

Generated by 🚫 Danger

@carrotkite

carrotkite commented Jun 3, 2026

Copy link
Copy Markdown

📊 Benchmark Comparison Report

Summary

  • Regressions: 0 ✅
  • Improvements: 21 🎉
  • Unchanged: 4

🎉 Performance Improvements

Benchmark Baseline Current Change
com.instacart.formula.benchmarks.TransitionQueueBenchmark.measure1 0.486 ± 0.003 us/op 0.318 ± 0.004 us/op -34.5% 🟢
com.instacart.formula.benchmarks.TransitionQueueBenchmark.measure100 0.488 ± 0.007 us/op 0.323 ± 0.008 us/op -33.9% 🟢
com.instacart.formula.benchmarks.TransitionQueueBenchmark.measure10000 0.492 ± 0.003 us/op 0.336 ± 0.003 us/op -31.7% 🟢
com.instacart.formula.benchmarks.ChildrenInitializationBenchmark.initializeNewChildren (childrenCount=1) 0.857 ± 0.09 us/op 0.613 ± 0.046 us/op -28.5% 🟢
com.instacart.formula.benchmarks.GlobalEffectQueueBenchmark.measure100Effects 10.835 ± 0.417 us/op 7.924 ± 0.397 us/op -26.9% 🟢
com.instacart.formula.benchmarks.ActionInitializationBenchmark.initializeNewActions (actionCount=1) 0.832 ± 0.026 us/op 0.63 ± 0.045 us/op -24.3% 🟢
com.instacart.formula.benchmarks.TransitionBenchmark.transitions (depth=0) 15.237 ± 0.076 us/op 11.551 ± 0.174 us/op -24.2% 🟢
com.instacart.formula.benchmarks.TransitionBenchmark.transitions (depth=10) 15.537 ± 0.081 us/op 11.786 ± 0.06 us/op -24.1% 🟢
com.instacart.formula.benchmarks.ActionCountBenchmark.stateChanges (actionCount=1) 15.31 ± 0.081 us/op 11.632 ± 0.065 us/op -24.0% 🟢
com.instacart.formula.benchmarks.CallbackOverheadBenchmark.transitions (callbackCount=50) 17.927 ± 1.928 us/op 13.66 ± 0.373 us/op -23.8% 🟢
com.instacart.formula.benchmarks.ChildrenCountBenchmark.stateChanges (childrenCount=1) 15.29 ± 0.191 us/op 11.653 ± 0.154 us/op -23.8% 🟢
com.instacart.formula.benchmarks.CallbackOverheadBenchmark.transitions (callbackCount=10) 15.69 ± 0.14 us/op 12.017 ± 0.098 us/op -23.4% 🟢
com.instacart.formula.benchmarks.ChildrenCountBenchmark.stateChanges (childrenCount=25) 15.787 ± 0.142 us/op 12.113 ± 0.05 us/op -23.3% 🟢
com.instacart.formula.benchmarks.ActionCountBenchmark.stateChanges (actionCount=25) 15.582 ± 0.175 us/op 11.965 ± 0.044 us/op -23.2% 🟢
com.instacart.formula.benchmarks.CallbackInitializationBenchmark.initializeNewCallbacks (callbackCount=10) 1.994 ± 0.184 us/op 1.533 ± 0.246 us/op -23.1% 🟢
com.instacart.formula.benchmarks.TransitionBenchmark.transitions (depth=20) 15.934 ± 0.108 us/op 12.319 ± 0.09 us/op -22.7% 🟢
com.instacart.formula.benchmarks.CallbackInitializationBenchmark.initializeNewCallbacks (callbackCount=50) 7.279 ± 1.991 us/op 5.639 ± 0.62 us/op -22.5% 🟢
com.instacart.formula.benchmarks.ActionCountBenchmark.stateChanges (actionCount=100) 16.764 ± 0.17 us/op 12.988 ± 0.114 us/op -22.5% 🟢
com.instacart.formula.benchmarks.ChildrenCountBenchmark.stateChanges (childrenCount=100) 17.345 ± 0.087 us/op 13.614 ± 0.077 us/op -21.5% 🟢
com.instacart.formula.benchmarks.ActionInitializationBenchmark.initializeNewActions (actionCount=25) 3.881 ± 0.063 us/op 3.18 ± 0.076 us/op -18.1% 🟢
com.instacart.formula.benchmarks.ChildrenInitializationBenchmark.initializeNewChildren (childrenCount=25) 3.922 ± 0.072 us/op 3.41 ± 0.059 us/op -13.1% 🟢
No significant changes (4 benchmarks)
Benchmark Baseline Current Change
com.instacart.formula.benchmarks.ActionInitializationBenchmark.initializeNewActions (actionCount=100) 12.411 ± 0.142 us/op 11.288 ± 0.024 us/op -9.1%
com.instacart.formula.benchmarks.ChildrenInitializationBenchmark.initializeNewChildren (childrenCount=100) 13.15 ± 0.185 us/op 12.238 ± 0.08 us/op -6.9%
com.instacart.formula.benchmarks.GlobalEffectQueueBenchmark.measure10Effects 0.764 ± 0.008 us/op 0.696 ± 0.005 us/op -8.8%
com.instacart.formula.benchmarks.GlobalEffectQueueBenchmark.measure1Effect 0.06 ± 0.001 us/op 0.056 ± 0.001 us/op -6.5%

Regressions: ±10% with non-overlapping confidence intervals. Improvements: ±10% change only.

Generated by 🚫 Danger

@alexanderbezverhni

Copy link
Copy Markdown
Collaborator Author

formula-android/src/main/java/com/instacart/formula/android/internal/ActivityManager.kt:77

ActivityStoreContextImpl now keeps route lifecycle state until a route reports DESTROYED, but ActivityManager is reused across configuration changes via AppManager. In the non-Fragment host path, there is no automatic equivalent of NavigationFlowRenderView.onFragmentViewDestroyed(), so on activity recreation this delegate can retain stale STARTED/RESUMED entries from the previous activity instance. That means isRouteStarted() / isRouteResumed() can replay true for routes that no longer exist until the new host explicitly re-drives every route. Should we clear the stored route lifecycle state here on activity destroy, or otherwise prove recreation is handled correctly for the new API?

Good catch - was addressed.

private var activity: Activity? = null
private var hasStarted: Boolean = false
private val fragmentLifecycleStates = mutableMapOf<String, Lifecycle.State>()
private val routeLifecycleStates = mutableMapOf<String, Lifecycle.State>()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few fragment -> route renames while I'm here

fun attachActivity(activity: Activity) {
hasStarted = false
this.activity = activity
clearRouteLifecycleState()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing on new Activity attach rather than on old Activity detach - to cover races when detach happens after attach.

Comment on lines +133 to +136
@MainThread
fun onRouteLifecycleStateChanged(routeId: RouteId<*>, state: Lifecycle.State) {
onRouteLifecycleState?.invoke(routeId, state)
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New public API for non-Fragment navigation hosts.

Comment on lines -6 to +8
import kotlinx.coroutines.rx3.asObservable
import kotlinx.coroutines.test.runTest

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing redundant Flow->Observable conversion and switching to turbine tests.

@alexanderbezverhni alexanderbezverhni marked this pull request as ready for review June 3, 2026 21:00
@pr-inbox-app

pr-inbox-app Bot commented Jun 3, 2026

Copy link
Copy Markdown

🚀 Ready for Review? 🚀

Mark your PR as ready and automatically assign the right reviewers.

Mark Ready Button
Let reviewers know this PR is ready for review

* is reused across configuration changes (see [AppManager]), without this the delegate would
* retain stale STARTED/RESUMED entries from the previous activity instance.
*/
fun clearRouteLifecycleState() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this logic - I worry it might break fragment logic in an unintuitive way.

@alexanderbezverhni alexanderbezverhni deleted the bezverhni/fragment_lifecycle branch June 8, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants