Fix FragmentManager and pendingOps race
FragmentNavigator adds pendingOps after triggering FragmentManager pop. This leads to a race where sometimes pendingOps are added after onBackStackChangeStarted callback, when ops should be added prior this callback. Now we add pendingOps before triggering FragmentManager pop.
Test: ./gradlew navigator:navigator-fragment:cC
Fixes: 326239315
Change-Id: I0259067c4ffed25c886d652c890b9ff7aeb0c47c
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index 4b9ad03..13c20d9 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -490,23 +490,27 @@
.isEqualTo(2)
assertThat(navigatorState.transitionsInProgress.value).isEmpty()
assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
-
+ // obs for upcoming pop
+ var pendingOps1: List<String> = emptyList()
fragmentNavigator.pendingOps.onBackStackChangedStarted {
- assertThat(it.entries()).containsExactly(entry2.id, entry3.id)
+ pendingOps1 = it.entries().toList()
}
// first pop entry3
fragmentNavigator.popBackStack(entry3, false)
+ assertThat(pendingOps1).containsExactly(entry2.id, entry3.id)
activityRule.runOnUiThread {
fragmentManager.executePendingTransactions()
assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
}
-
+ // obs for upcoming pop
+ var pendingOps2: List<String> = emptyList()
fragmentNavigator.pendingOps.onBackStackChangedStarted {
- assertThat(it.entries()).containsExactly(entry1.id, entry2.id)
+ pendingOps2 = it.entries().toList()
}
// And then pop entry2
fragmentNavigator.popBackStack(entry2, false)
+ assertThat(pendingOps2).containsExactly(entry1.id, entry2.id)
activityRule.runOnUiThread {
fragmentManager.executePendingTransactions()
assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
@@ -1729,24 +1733,26 @@
}
})
}
-
+ // obs for upcoming pop
+ var pendingOps1: List<String> = emptyList()
fragmentNavigator.pendingOps.onBackStackChangedStarted {
- assertThat(it.entries()).containsExactly(entry2.id, entry3.id)
+ pendingOps1 = it.entries().toList()
}
fragmentNavigator.popBackStack(entry3, false)
+ assertThat(pendingOps1).containsExactly(entry2.id, entry3.id)
assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
-
+ // obs for upcoming pop
+ var pendingOps2: List<String> = emptyList()
fragmentNavigator.pendingOps.onBackStackChangedStarted {
- assertThat(it.entries()).containsExactly(entry1.id, entry2.id)
+ pendingOps2 = it.entries().toList()
}
fragmentNavigator.popBackStack(entry2, false)
-
assertThat(navigatorState.backStack.value).containsExactly(entry1)
activityRule.runOnUiThread {
fragmentManager.executePendingTransactions()
assertThat(fragmentNavigator.pendingOps.entries()).isEmpty()
}
-
+ assertThat(pendingOps2).containsExactly(entry1.id, entry2.id)
val fragment1 = fragmentManager.findFragmentById(R.id.container) as AnimatorFragment
assertWithMessage("Fragment should be added")
.that(fragment1)
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
index f58d911..80a18ce 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
@@ -305,6 +305,22 @@
beforePopList.size
)
val initialEntry = beforePopList.first()
+
+ // add pending ops here before any animation (if present) or FragmentManager work starts
+ val incomingEntry = beforePopList.elementAtOrNull(popUpToIndex - 1)
+ if (incomingEntry != null) {
+ addPendingOps(incomingEntry.id)
+ }
+ poppedList.filter { entry ->
+ // normally we don't add initialEntry to pending ops because the adding/popping
+ // of an isolated fragment does not trigger onBackStackCommitted. But if initial
+ // entry was already added to pendingOps, it was likely an incomingEntry that now
+ // needs to be popped, so we need to overwrite isPop to true here.
+ pendingOps.asSequence().map { it.first }.contains(entry.id) ||
+ entry.id != initialEntry.id
+ }.forEach { entry ->
+ addPendingOps(entry.id, isPop = true)
+ }
if (savedState) {
// Now go through the list in reversed order (i.e., started from the most added)
// and save the back stack state of each.
@@ -333,22 +349,6 @@
)
}
- val incomingEntry = beforePopList.elementAtOrNull(popUpToIndex - 1)
- if (incomingEntry != null) {
- addPendingOps(incomingEntry.id)
- }
- // add pending ops here before any animation (if present) starts
- poppedList.filter { entry ->
- // normally we don't add initialEntry to pending ops because the adding/popping
- // of an isolated fragment does not trigger onBackStackCommitted. But if initial
- // entry was already added to pendingOps, it was likely an incomingEntry that now
- // needs to be popped, so we need to overwrite isPop to true here.
- pendingOps.asSequence().map { it.first }.contains(entry.id) ||
- entry.id != initialEntry.id
- }.forEach { entry ->
- addPendingOps(entry.id, isPop = true)
- }
-
state.popWithTransition(popUpTo, savedState)
}