Status Update
Comments
il...@google.com <il...@google.com>
il...@google.com <il...@google.com>
ap...@google.com <ap...@google.com> #2
Please include a sample project using Navigation 2.4.0-rc01 that reproduces your issue.
ap...@google.com <ap...@google.com> #3
Thank you but this version can still be reproduced,here is the project that can be reproduced:
- enable the setting of "don't keep activity" in developer options.
- start app, click "About" button.
- press HOME button.
- click app icon in launcher again.
ga...@gmail.com <ga...@gmail.com> #4
Thanks, the sample project was really helpful. While the behavior here could be considered 'technically correct', this is unintentional and we'll look to see what we can do to improve this case.
What's actually going on
This looks like a case where there's a marked difference in behavior between fragment lifecycle callbacks and
Specifically, there are three elements in play:
- The
NavHostFragment
- The
NavController
and theNavBackStackEntry
for each entry in its back stack - The child
Fragment
for each entry in the back stack that corresponds with a fragment destination
When you first load your activity, the NavHostFragment
is in its initial state (INITIALIZED
in the AndroidX Lifecycle terms). In the NavHostFragment
's onCreate()
, it sets the graph you've set via the app:navGraph="@navigation/nav_graph"
in your activity_main.xml
on the NavController
it owns. This is what causes Fragments to be added to the NavHostFragment
's childFragmentManager
.
Because of how Fragments work currently, the fragment lifecycle callbacks for upward transitions (like onCreate()
) are called before the AndroidX Lifecycle is moved to that same state:
NavHostFragment
gets itsonCreate()
calledNavHostFragment
inflates its navigation graph and adds theTitle
fragment to thechildFragmentManager
- Fragments call
onCreate()
on their child fragments (i.e.,Title
) NavHostFragment
's Lifecycle is moved toCREATED
- Fragments move their child fragments to
CREATED
The same thing happens for onStart()
+STARTED
and onResume()
+RESUMED
.
This all works as expected when dealing with only Fragments - the parent fragment gets onCreate()
before its child fragments. That nesting is also done on the AndroidX Lifecycle side when it comes to the Fragment's Lifecycle. That idea of nesting Lifecycle changes between parent and child is a key part of the whole infrastructure. The goal is that at no point does a 'child' have a higher Lifecycle state as its parent.
NavController
depends solely on AndroidX Lifecycle to drive the Lifecycle of each NavBackStackEntry
. And where does that NavController
gets its AndroidX Lifecycle callbacks from? From the NavHostFragment
that it has been created by. This means that what is actually happening adds an additional line:
NavHostFragment
gets itsonCreate()
calledNavHostFragment
inflates its navigation graph and adds theTitle
fragment to thechildFragmentManager
- Fragments call
onCreate()
on their child fragments (i.e.,Title
) NavHostFragment
's Lifecycle is moved toCREATED
NavController
moves eachNavBackStackEntry
toCREATED
- Fragments move their child fragments to
CREATED
This is where your error message comes in: you're accessing a NavBackStackEntry
's ViewModel in step 3 which is before it actually moves to CREATED
(that's step 5).
Caused by: java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry reaches the CREATED state).
You'd find you'd get the same exact error message if you moved your by navGraphViewModels()
code to the Title
fragment. You just also see the same issue after turning on 'Don't keep activities' since when recreating the entire activity, all child fragments (including both Title
and About
) are going through these same steps of having their onCreate()
called before the NavHostFragment
has had its AndroidX Lifecycle moved to CREATED
. This is also explains why it works when you navigate
to About
manually - by that point, the NavHostFragment
is RESUMED
and each NavBackStackEntry
is able to move beyond the CREATED
state immediately as part of that call to navigate()
.
Workarounds
So if you want to run your code after the NavController
moves each NavBackStackEntry
to CREATED
, you'll want to move your code to step 6 and use an LifecycleObserver
.
Using the DefaultLifecycleObserver
class is by far the easiest way to do this. By upgrading to Lifecycle 2.4.0
(by updating the version in versions.gradle
and adding an explicit dependency on implementation deps.lifecycle.runtime
), you could rewrite your About
fragment as:
class About : Fragment(), DefaultLifecycleObserver {
private val vm by navGraphViewModels<TestViewModel>(R.id.home)
// Note how this has a different signature from the Fragment method of the same name
// this signature is the one from DefaultLifecycleObserver
override fun onCreate(owner: LifecycleOwner) {
Log.d("About", "About string ${vm.name}")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_about, container, false)
}
}
By moving to AndroidX Lifecycle callbacks, you ensure that your child fragment calls always happen after the parent NavHostFragment
and its NavController
have moved through that same Lifecycle state.
Next steps on our side
As per the error message, the intent is that you cannot access the ViewModels until the NavBackStackEntry
has been added to the NavController
's back stack. However, in this case, the NavBackStackEntry
is on the back stack (that's why by navGraphViewModels()
was able to find the NavBackStackEntry
associated with R.id.home
in the first place - you wouldn't have been able to get to this error message if you had chosen an ID that didn't exist on the back stack).
Therefore there might be more clever logic we can do to cover the 'added to back stack but not yet CREATED
' case to allow for this type of call from a fragment's onCreate()
to succeed. I'll leave this issue open to cover exactly that work.
Future state
There's ongoing work being done in onCreate()
would only occur when your AndroidX Lifecycle has moved to CREATED
, effectively removing the difference between the two types of callbacks. I'd strongly suggest staring that issue as well to track when that work will be available.
fo...@google.com <fo...@google.com> #5
Thanks, I've learnt a lot and you save my life, I'll keep an eye on this issue until there're any updates.
ap...@google.com <ap...@google.com> #6
Branch: androidx-main
commit 2560e8fa99d8e13c5907375e69759520fefc8dd5
Author: Ian Lake <ilake@google.com>
Date: Wed Jan 19 00:36:30 2022
Fix getViewModelStore() prior to ON_CREATE
The intention is that the ViewModels associated
with a NavBackStackEntry cannot be accessed prior
to the NavBackStackEntry being added to the
NavController. We were previously using the ON_CREATE
event to signify when this happens, but in fact there are
cases, such as in a Fragment's onCreate(), where a
NavBackStackEntry can be retrieved from the NavController
*before* the Lifecycle reaches CREATED (due to how
fragment lifecycle callbacks relate to Lifecycle changes).
By performing the restore exactly once the first time
updateState() is called, we ensure that ViewModels are
accessible immediately after being added to the NavController.
Test: updated test suites
BUG: 213504272
Relnote: "Fixed an issue where accessing a ViewModel created
via `by navGraphViewModels()` from a Fragment's `onCreate()`
would fail with an `IllegalStateException`."
Change-Id: I8a14dd596195d4ddfa8199c8023a7aedcd286113
M navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
M navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
M navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavHostFragmentTest.kt
ap...@google.com <ap...@google.com> #7
We've fixed this issue internally and it will be available in the upcoming Navigation 2.5.0-alpha01 and Navigation 2.4.1 releases.
Description
When defining an explicit deep-link using NavDeepLinkBuilder, only a single Argument bundle can be provided for every Fragment destination inferred from XML. This can cause conflicts between arguments of the same name shared by different Destinations, and no argument verification is performed, so it's possible for parent Fragments to crash at navigateUp() time.
Request: A NavDeepLinkBuilder api that allows composition of explicit [destId, args] pairs to construct a destination stack. Preferably, accepting NavDirections as the parameter, so existing SafeArgs generated wrappers can be reused for argument safety.