Accessing graph-scoped ViewModel of child NavHostFragment using by navGraphViewModels - android

I am using the Navigation Component of Android Jetpack (2.2.0-alpha01).
I wish to use a child NavHostFragment nested inside my main NavHostFragment, equipped with its own child nav graph. Please view the following image for context:
The child nav host is defined like this inside the fragment that is at the front of the MainNavHost's stack:
<fragment
android:id="#+id/childNavHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="false"
app:navGraph="#navigation/child_graph" />
Inside the fragment that is at the front of the CHILD Nav Host Fragment, I am trying to get a ViewModel scoped to the R.navigation.child_graph by using the following code:
private val childGraphScopedViewModel: ChildGraphScopedViewModel by navGraphViewModels(R.navigation.child_graph) {
viewModelFactory
}
When accessing the childGraphScopedViewModel, I am getting a crash with the error message:
java.lang.IllegalArgumentException: No NavGraph with ID 2131689472 is on the NavController's back stack.
I believe the lazy init call by navGraphViewModel() is looking for the navgraph inside the mainGraph.
How can I access a child navHostFragment scoped ViewModel? Thank you for your time.

This works for me without defining aby custom ViewModelProvider (2.2.0):
private val viewModel: ChildGraphScopedViewModel by navGraphViewModels(R.id. child_graph)
An Easy mistake to make is to use R.navigation.child_graph (bad) instead of R.id.child_graph (good)

You can do that by providing the viewModelStore of child NavController
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
val childHostFragment = childFragmentManager
.findFragmentById(R.id.childNavHostFragment) as NavHostFragment
val childNavController = childHostFragment.navController
val childViewModel: ChildGraphScopedViewModel = ViewModelProvider(
childNavController.getViewModelStoreOwner(R.navigation.child_graph)
).get(ChildGraphScopedViewModel::class.java)
}
I wrote a Kotlin Extension for making it easier
inline fun <reified T: ViewModel> NavController.viewModel(#NavigationRes navGraphId: Int): T {
val storeOwner = getViewModelStoreOwner(navGraphId)
return ViewModelProvider(storeOwner)[T::class.java]
}
Usage
val viewModel = findNavController().viewModel(R.navigation.nav)

For some reason creating the nav graph and using include to nest it inside the main nav graph was not working for me. Probably it is a bug.
You can select all the fragments that need to be grouped together inside the nav graph and right-click->move to nested graph->new graph
now this will move the selected fragments to a nested graph inside the main nav graph like this:
<navigation app:startDestination="#id/homeFragment" ...>
<fragment android:id="#+id/homeFragment" .../>
<fragment android:id="#+id/productListFragment" .../>
<fragment android:id="#+id/productFragment" .../>
<fragment android:id="#+id/bargainFragment" .../>
<navigation
android:id="#+id/checkout_graph"
app:startDestination="#id/cartFragment">
<fragment android:id="#+id/orderSummaryFragment".../>
<fragment android:id="#+id/addressFragment" .../>
<fragment android:id="#+id/paymentFragment" .../>
<fragment android:id="#+id/cartFragment" .../>
</navigation>
</navigation>
Now, inside the fragments when you initialize the ViewModel do this
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
If you need to pass the viewmodel factory(may be for injecting the viewmodel) you can do it like this:
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }

Related

navigation disconnect fragments and view

I have navigation competent just go from Fragment A to B , however going back from B to A , making A loose everything, text field data, click linstners, there is code inside oncreeteView to init view also does be called , ViewModel live data no more active
<fragment
android:id="#+id/booking_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/appBarLayout"
app:navGraph="#navigation/nav_graph" />
Oncreate
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.booking_nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(
topLevelDestinationIds = setOf( R.id.bookingFragment),
fallbackOnNavigateUpListener = ::onSupportNavigateUp
)
toolbar_details.setupWithNavController(navController, appBarConfiguration)
setSupportActionBar(toolbar_details)
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
In nav_graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/a">
<fragment
android:id="#+id/a"
android:name=".A"
android:label=" ">
<action
android:id="#+id/action_a_to_b"
app:destination="#id/b" />
Fragment A :
private val viewModelFilter: filterViewModel by sharedViewModel()
do you have an Activity to hold the fragments or it's just two fragments A and B?
I think the easy way to use one based Main Activity having the fragments on it
and init the main view Model in the Activity and shear it at the fragments so you keep the data alive because its lifecycle ended with the fragment associated with it
And since the viewModel expires and onCleared runs at the end of the life of the fragment from which the information is reduced, so it must be created in the Activity so that it remains running and the fragment is reduced from it.
here is an example code :
1- in main Activity
viewModel=ViewModelProvider(this).get(ViewModel::class.java)
2- in the first fragment you to get the same viewModel instance from the main actitivty
activity.let {
viewModel=ViewModelProvider(it!!).get(ViewModel::class.java)
}
and this way you get the view Model alive throw the fragment's lifecycle and destroyed when the activity is finished

How to use NavHostFragment with FragmentContainerView?

I’m curious about an issue I’m having with Navigation Component, specifically on hosting NavHostFragment. I have a single-Activity native app with a few Fragments. I’m in the launcher’s xml, replacing the <fragment> with <androidx.fragment.app.FragmentContainerView>.
The initial implementation has worked as intended for a year. While introducing the FragmentContainerView this week the starting destination’s screen doesn’t render at launch. Yet I observe logs that prove successful data incoming from network and cache. From this state, only triggering configuration change by rotating the device renders UI as expected.
My AndroidX NavigationFragmentKtx and NavigationUiKtx dependencies are version 2.3.2. Relevant files abbreviated. Does anything stand out that I've missed? Any thoughts are appreciated.
launcher_activity.xml
<androidx.fragment.app.FragmentContainerView
android:id="#+id/launcher_navHost"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/nav_graph" />
LauncherActivity.kt
class LauncherActivity : AppCompatActivity(R.layout.launcher_activity) {
private lateinit var viewBinding: LauncherActivityBinding
private val navController: NavController by lazy {
val navHost = supportFragmentManager.findFragmentById(
R.id.launcher_navHost
) as NavHostFragment
navHost.navController
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = LauncherActivityBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
}
}
You're using AppCompatActivity(R.layout.launcher_activity), which means you are automatically calling setContentView(R.layout.launcher_activity) as part of super.onCreate(savedInstanceState) (that's what passing a layout ID does). You're then overriding that layout with a second call to setContentView().
You can just remove the R.layout.launcher_activity part if you're using View Binding as it is not needed (and the root of your issue):
class LauncherActivity : AppCompatActivity() {

by navGraphViewModels creates java.lang.IllegalArgumentException: No destination with ID <destination id> is on the NavController's back stack

When a configuration change happens and my Activity and Fragment are recreated because of it, my nav Graph scoped ViewModel is unavailable while the Fragments have already been created again.
It seems like the Fragment gets recreated, before the navGraph does.
I am using this code to initialize my navGraph scoped ViewModel from my Fragment:
private val myViewModel: MyViewModel by navGraphViewModels(R.id.nav_graph_id)
If I try to use myViewModel in the Fragments onViewCreated function, I get a IllegalArgumentException after a Configuration change. The Exception:
java.lang.IllegalArgumentException: No destination with ID <destination id> is on the NavController's back stack
How do I handle this?
I have already checked that my ID isn't used anywhere else.
Edit1:
Here is my activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/main_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:navGraph="#navigation/main_nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
And here is my main_nav_graph.xml:
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main_nav_graph"
app:startDestination="#id/main_nav_graph_1">
<navigation
android:id="#+id/main_nav_graph_1"
android:label="#string/nav_graph_1_label"
app:startDestination="#id/nav_graph_1_start_fragment">
<!-- nav graph stuff -->
</navigation>
<navigation
android:id="#+id/main_nav_graph_2"
android:label="#string/nav_graph_2_label"
app:startDestination="#id/nav_graph_2_start_fragment">
<fragment
android:id="#+id/nav_graph_2_start_fragment"
android:label="#string/nav_graph_2_start_fragment_label"
android:name="my.package.ui.NavGraph2StartFragment"
tools:layout="#layout/fragment_nav_graph_2_start">
</fragment>
<!-- In here is where I get the problem -->
</navigation>
</navigation>
My Problem was the following:
After a Configuration change the NavGraph returned to its start destination, but the Fragment that was last active gets loaded anyway. This meant that the Fragment that was actually started and the current destination of the navGraph were out of sync.
When my Fragment then tried to load the navGraph scoped ViewModel, it failed because the navGraph thought it was on a different Fragment then it actually was.
To fix my problem I had to save and restore the state of the navController using the activities savedInstanceState Bundle. (source: https://stackoverflow.com/a/59987336)
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBundle("nav_state", fragment.findNavController().saveState())
}
// restore in RestoreInstanceState
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
fragment.findNavController().restoreState(savedInstanceState.getBundle("nav_state"))
}
// or restore in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState?.containsKey("nav_state") == true) {
fragment.findNavController().restoreState(savedInstanceState.getBundle("nav_state"))
}
}
For anyone else coming here, I experienced this issue when a dialog was currently displayed and I issued a navController.navigate() call to a Fragment which uses by navGraphViewModels() before the dialog was dismissed.
It's as if the Navigation library couldn't understand that there is a destination with that ID available when a dialog is being displayed.
And for more clarity, this dialog is displayed using the <dialog> attribute in my nav graph XML.
My fix (for now) is to ensure the dialog is dismissed before making a further call to navigate elsewhere by way of view.postDelayed({ navController.navigate(elsewhere)}, 300)
This isn't the first time I've experienced issues with the NavController trying to navigate when a dialog is displayed.
Solved Easy by change this :
private val myViewModel: MyViewModel by hiltNavGraphViewModels(R.id.nav_graph_id)
To:
private val viewModel: AuthViewModel by activityViewModels{ defaultViewModelProviderFactory }

Navigation Host becomes null while navigating (Android Navigation Component)

I am creating a game where the user goes through a series of 5 screens. At the last screen, the user has the choice to end the game, at which point they are taken back to the starting screen. My problems come in when a user ends the game and then starts again. While navigating through the app, the navigation host fragment cannot be found.
The first time through the app, it navigates at usual, but the second time, the navigation host cannot be found.
I have tried using different views to find the navigation host, and while debugging, I saw that for the fragment where it can not be found, the parent is equal to null.
This is where I navigate, in the fragments onViewCreated()
viewModel.getGameUpdates().observe(activity!!, Observer { updatedGame ->
if(updatedGame.playerList.size == 0){
Log.d("END","END")
viewModel.endGame()
}
adapter?.players = updatedGame.playerList
if(updatedGame.started){
Navigation.findNavController(view).navigate(R.id.action_waitingFragment_to_gameFragment)
}
})
and this is the moment where the user clicks to navigate back to the first screen:
btn_end_game.setOnClickListener {
viewModel.endGame()
timer.cancel()
Navigation.findNavController(view).navigate(R.id.action_gameFragment_to_startFragment)
}
The layout for my MainActivity that holds the navigation host fragment is:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</FrameLayout>
I do realize that I am just adding on top of the back stack when I would rather pop back to the first fragment. I am just lost as to how the fragment is null.
The following is the nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/nav_graph" app:startDestination="#id/startFragment">
<fragment android:id="#+id/startFragment" android:name="com.dangerfield.spyfall.start.StartFragment"
android:label="StartFragment">
<action android:id="#+id/action_startFragment_to_joinGameFragment" app:destination="#id/joinGameFragment"/>
<action android:id="#+id/action_startFragment_to_newGameFragment" app:destination="#id/newGameFragment"/>
</fragment>
<fragment android:id="#+id/newGameFragment" android:name="com.dangerfield.spyfall.newGame.NewGameFragment"
android:label="NewGameFragment">
<action android:id="#+id/action_newGameFragment_to_waitingFragment" app:destination="#id/waitingFragment"/>
</fragment>
<fragment android:id="#+id/joinGameFragment" android:name="com.dangerfield.spyfall.joinGame.JoinGameFragment"
android:label="JoinGameFragment">
<action android:id="#+id/action_joinGameFragment_to_waitingFragment" app:destination="#id/waitingFragment"/>
</fragment>
<fragment android:id="#+id/waitingFragment" android:name="com.dangerfield.spyfall.waiting.WaitingFragment"
android:label="WaitingFragment">
<action android:id="#+id/action_waitingFragment_to_gameFragment" app:destination="#id/gameFragment"/>
<action android:id="#+id/action_waitingFragment_to_startFragment" app:destination="#id/startFragment"/>
</fragment>
<fragment android:id="#+id/gameFragment" android:name="com.dangerfield.spyfall.game.GameFragment"
android:label="GameFragment">
<action android:id="#+id/action_gameFragment_to_startFragment" app:destination="#id/startFragment"/>
</fragment>
</navigation>
This is the message given after crash:
java.lang.IllegalStateException: View android.widget.ScrollView{637e4ce VFED.V... ......ID 0,0-1440,2308} does not have a NavController set
LiveData remembers the current data and will automatically redeliver it when the observer becomes started again, making it inappropriate for events that trigger navigation operations: your operation to navigate() is going to be triggered every time your Fragment is started, making it impossible to actually pop back to that Fragment.
Note that Fragments are not destroyed while on the back stack. If you're changing the underlying data that your Fragment relies on while that Fragment is on the back stack, you should use the viewLifecycleOwner instead of this (representing the Fragment) for your LifecycleOwner passed to observe() when observing in onViewCreated(). This ensures that you will no longer get observer callbacks once your view is destroyed (i.e., you go onto the back stack).
activity!! is absolutely always wrong to use as the LifecycleOwner from within a Fragment, since that means the observer will not be cleaned up even if the Fragment is completely destroyed (it'll only be cleaned up when the activity is destroyed).
As per the conditional navigation documentation, the recommended approach is to ensure that your LiveData is tracking state rather than events. That way, after you call navigate(), you can update the state to ensure that when the callback happens a second time, you don't call navigate() a second time. This approach is recommended over the SingleLiveEvent approach.
Even I Was facing the same issue when I used to navigate from current fragment to the next fragment, and on the back press of hardware navHost would be null, The mistake I was doing is that I had made the variable navController global where I used to instantiate like this in onCreate()
Before:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this,navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
After:
now it's working after this change
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this,navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return navController.navigateUp()
}
}
Dont why navController would be null if made it global??

Android Architechture Navigation - onSupportNavigateUp()

While using Navigation Library, is it okay to finish the inner activity with finish() in onSupportNavigateUp()? Or we should use NavController to remove that Activity from Stack? And, I have trouble using NavController to remove Activity.
My Navigation graph looks like below:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="#id/home_frag">
<fragment
android:id="#+id/home_frag"
android:name="com.yamikrish.app.slicedemo.ui.home.HomeFragment"
android:label="home_frag"
tools:layout="#layout/home_fragment">
<action
android:id="#+id/open_details"
app:destination="#id/details_fragment" />
</fragment>
<activity
android:id="#+id/details_fragment"
android:name="com.yamikrish.app.slicedemo.ui.detail.DetailActivity"
android:label="#string/post_detail"
tools:layout="#layout/detail_page">
<argument
android:name="id"
app:type="integer" />
</activity>
<fragment
android:id="#+id/profile_frag"
android:name="com.yamikrish.app.slicedemo.ui.profile.ProfileFragment"
android:label="#string/profile"
tools:layout="#layout/profile_fragment" />
</navigation>
I have tried to use like below:
(i) Using NavController inside DetailActivity
override fun onSupportNavigateUp(): Boolean {
val nav = NavController(this)
return nav.navigateUp()
}
But getting exception as,
java.lang.IllegalArgumentException: NavController back stack is empty
(ii) Using NavController inside BaseActivity
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.container).navigateUp()
}
That also not working. Am I doing it wrong??
Your detail activity has no concept of the parent nav graph in this case. You example ii is correct. Since BaseActivity contains a navigation graph. I don't know what the DetailActivity is doing but if it also contains a navigation graph then you should use the same code as your BaseActivity. The fact that you're getting that error and also creating a new NavController on the fly tells me that DetailActivity is not using a navigation graph (creating a new NavController and not providing it with a graph will by default initialize with an empty back stack).
In summary, DetailActivity's onSupportNavigateUp should behave like a normal up navigation. If there's a clear parent you should launch it, otherwise, just finish().

Categories

Resources