Skip to content

Commit 320aa4c

Browse files
author
Jeremy Woods
committed
Ensure FragmentNavigator does not destroy entries early
There is a case where if you navigate to a destination and the fragment that is assocated with the destination does a navigate with popUpTo in its onResume() lifecycle callback, the FragmentNavigator incorrectly destroys the entry before the associated fragment is destroyed. The reason for this is the timing between fragment lifecycle callbacks and Lifecycle. The onResume() lifecycle callback happens immediately before the `ON_RESUME` Lifecycle event and once the lifecycle listeners in FragmentNavigator get the `ON_RESUME` event, the state of the entry has changed and gets destroyed. Instead of always marking entries as complete when we get the end Lifecycle events (ON_RESUME and ON_DESTROY) from the fragment, we should check whether the back stack contains the entry or not. RelNote: "Fixed an issue in Navigation with fragments where removing a fragment via navigate with popUpTo in its `onResume()` lifecycle callback would cause an `IllegalStateException`." Test: added test Bug: 279644470 Change-Id: I21884c268cf213caacabe19db38fd49a1ac829fa
1 parent c2a537d commit 320aa4c

File tree

2 files changed

+46
-26
lines changed

2 files changed

+46
-26
lines changed

navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import androidx.lifecycle.Lifecycle
2424
import androidx.lifecycle.ViewModel
2525
import androidx.lifecycle.ViewModelProvider
2626
import androidx.navigation.NavOptions
27+
import androidx.navigation.createGraph
28+
import androidx.navigation.fragment.test.EmptyFragment
2729
import androidx.navigation.fragment.test.NavigationActivity
2830
import androidx.navigation.fragment.test.R
2931
import androidx.navigation.navOptions
@@ -284,6 +286,20 @@ class NavControllerWithFragmentTest {
284286
assertThat(fm.fragments.size).isEqualTo(2) // start + last dialog fragment
285287
}
286288

289+
@Test
290+
fun testPopEntryInFragmentResumed() = withNavigationActivity {
291+
navController.graph = navController.createGraph("first") {
292+
fragment<EmptyFragment>("first")
293+
fragment<PopInOnResumeFragment>("second")
294+
}
295+
navController.navigate("second")
296+
297+
val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
298+
fm?.executePendingTransactions()
299+
300+
assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("first")
301+
}
302+
287303
private fun withNavigationActivity(
288304
block: NavigationActivity.() -> Unit
289305
) {
@@ -295,6 +311,15 @@ class NavControllerWithFragmentTest {
295311
}
296312
}
297313

314+
class PopInOnResumeFragment : Fragment(R.layout.strict_view_fragment) {
315+
override fun onResume() {
316+
super.onResume()
317+
findNavController().navigate("first") {
318+
popUpTo("first")
319+
}
320+
}
321+
}
322+
298323
class TestDialogFragment : DialogFragment() {
299324
val dialogs = mutableListOf<Dialog>()
300325

navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager.OnBackStackChangedListener
2929
import androidx.fragment.app.FragmentTransaction
3030
import androidx.lifecycle.Lifecycle
3131
import androidx.lifecycle.LifecycleEventObserver
32-
import androidx.lifecycle.LifecycleOwner
3332
import androidx.lifecycle.ViewModel
3433
import androidx.lifecycle.ViewModelProvider
3534
import androidx.lifecycle.viewmodel.CreationExtras
@@ -75,38 +74,34 @@ public open class FragmentNavigator(
7574
*/
7675
internal val backStack get() = state.backStack
7776

78-
private val fragmentObserver = object : LifecycleEventObserver {
79-
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
80-
if (event == Lifecycle.Event.ON_DESTROY) {
81-
val fragment = source as Fragment
82-
val entry = state.transitionsInProgress.value.lastOrNull { entry ->
83-
entry.id == fragment.tag
84-
}
85-
entry?.let {
86-
entriesToPop.remove(entry.id)
87-
state.markTransitionComplete(it)
77+
private val fragmentObserver = LifecycleEventObserver { source, event ->
78+
if (event == Lifecycle.Event.ON_DESTROY) {
79+
val fragment = source as Fragment
80+
val entry = state.transitionsInProgress.value.lastOrNull { entry ->
81+
entry.id == fragment.tag
82+
}
83+
if (entry != null) {
84+
entriesToPop.remove(entry.id)
85+
if (!state.backStack.value.contains(entry)) {
86+
state.markTransitionComplete(entry)
8887
}
89-
fragment.lifecycle.removeObserver(this)
9088
}
9189
}
9290
}
9391

9492
private val fragmentViewObserver = { entry: NavBackStackEntry ->
95-
object : LifecycleEventObserver {
96-
override fun onStateChanged(
97-
source: LifecycleOwner,
98-
event: Lifecycle.Event
99-
) {
100-
// Once the lifecycle reaches RESUMED, we can mark the transition complete
101-
if (event == Lifecycle.Event.ON_RESUME) {
102-
state.markTransitionComplete(entry)
103-
}
104-
// Once the lifecycle reaches DESTROYED, we can mark the transition complete and
105-
// remove the observer.
106-
if (event == Lifecycle.Event.ON_DESTROY) {
107-
entriesToPop.remove(entry.id)
93+
LifecycleEventObserver { _, event ->
94+
// Once the lifecycle reaches RESUMED, if the entry is in the back stack we can mark
95+
// the transition complete
96+
if (event == Lifecycle.Event.ON_RESUME && state.backStack.value.contains(entry)) {
97+
state.markTransitionComplete(entry)
98+
}
99+
// Once the lifecycle reaches DESTROYED, if the entry is not in the back stack, we can
100+
// mark the transition complete
101+
if (event == Lifecycle.Event.ON_DESTROY) {
102+
entriesToPop.remove(entry.id)
103+
if (!state.backStack.value.contains(entry)) {
108104
state.markTransitionComplete(entry)
109-
source.lifecycle.removeObserver(this)
110105
}
111106
}
112107
}

0 commit comments

Comments
 (0)