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
Related
I use Jetpack Navigation for navigating between fragments and activity. So, I have a MainActivity with a FragmentContainerView in the layout. I can easily navigate from fragment to fragment/activity. But, I can't find a way how to navigate from one activity to another activity/fragment with navController.
For example, from fragment FA, I call navController.navigate() to activity A. Now, I want to navigate from activity A to activity B or fragment FB.
I already tried this:
val host = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = host.navController
But always got this error
null cannot be cast to non-null type androidx.navigation.fragment.NavHostFragment
Thanks!
A note from the navigation component guide
Note: The Navigation component is designed for apps that have one main activity with multiple fragment destinations. The main activity is associated with a navigation graph and contains a NavHostFragment that is responsible for swapping destinations as needed. In an app with multiple activity destinations, each activity has its own navigation graph.
You shouldn't use the navigation component to navigate from one activity to another. It is made to swap fragments on a Fragment container.
You can get navController inside activity using
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
You also have to add android:name="androidx.navigation.fragment.NavHostFragment" to your fragment container
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
For java coder;
it is possible to get NavController instance this way
NavHostFragment navHostFragment =(NavHostFragment)getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
I am using navigation component and BottomNavigationView, i am facing a problem, that is when i go to from fragment 1>2>5>4>3 and when i press back button i get fragment 1. I know this is the default behavior but i don't want this, i want to save them in backstack so when i press back button it should go to fragment 4 not 1. I have been trying and searching but i couldn't find any solution. Can i put fragment manually into backstack in kotlin?
My code:
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
.......................
.......................>
<androidx.appcompat.widget.Toolbar
................
................/>
<fragment
android:id="#+id/app_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="#navigation/app_nav"
..............
............../>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/app_bottom_nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#drawable/white_grey_border_bottom"
app:menu="#menu/bottom_nav_menu"
app:labelVisibilityMode="unlabeled"
...........
.........../>
</androidx.constraintlayout.widget.ConstraintLayout>
app_nav.xml
<navigation
...........
...........
android:id="#+id/app_nav"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.example.instagram_clone.ui.HomeFragment"
android:label="HomeFragment"
tools:layout="#layout/fragment_home"/>
.............
.............
.............
.............
</navigation>
MainActivity.kt
class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val bottomNavView = binding.appBottomNavView
val navController = findNavController(R.id.app_nav_host_fragment)
bottomNavView.setupWithNavController(navController)
}
}
As per the documentation on onNavDestinationSelected() (the method that setupWithNavController() uses when selecting a MenuItem):
By default, the back stack will be popped back to the navigation graph's start destination. Menu items that have android:menuCategory="secondary" will not pop the back stack.
So just add android:menuCategory="secondary" to each of the menu items used with your BottomNavigationView.
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 }
I am using navigation component and BottomNavigationView, i am facing a problem, that is when i go to from fragment 1>2>5>4>3 and when i press back button i get fragment 1. I know this is the default behavior but i don't want this, i want to save them in backstack so when i press back button it should go to fragment 4 not 1. I have been trying and searching but i couldn't find any solution. Can i put fragment manually into backstack in kotlin?
My code:
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
.......................
.......................>
<androidx.appcompat.widget.Toolbar
................
................/>
<fragment
android:id="#+id/app_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="#navigation/app_nav"
..............
............../>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/app_bottom_nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#drawable/white_grey_border_bottom"
app:menu="#menu/bottom_nav_menu"
app:labelVisibilityMode="unlabeled"
...........
.........../>
</androidx.constraintlayout.widget.ConstraintLayout>
app_nav.xml
<navigation
...........
...........
android:id="#+id/app_nav"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.example.instagram_clone.ui.HomeFragment"
android:label="HomeFragment"
tools:layout="#layout/fragment_home"/>
.............
.............
.............
.............
</navigation>
MainActivity.kt
class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val bottomNavView = binding.appBottomNavView
val navController = findNavController(R.id.app_nav_host_fragment)
bottomNavView.setupWithNavController(navController)
}
}
As per the documentation on onNavDestinationSelected() (the method that setupWithNavController() uses when selecting a MenuItem):
By default, the back stack will be popped back to the navigation graph's start destination. Menu items that have android:menuCategory="secondary" will not pop the back stack.
So just add android:menuCategory="secondary" to each of the menu items used with your BottomNavigationView.
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??