I have a complicated structure where a number of Fragments link to each other, so using NavController I am trying to avoid creating multiple duplicates of the same Fragment in the BackStack.
I found this post How to check Navigation Destination is in the NavController back stack or not?, which I have implemented as below:
private fun onSiteItemClicked(item: SiteObject) {
Log.d(TAG, "onItemClicked() - ${item.siteReference}")
item.siteID.let {
businessViewModel.updateCurrentSiteVMLiveData(it)
try {
val backStackEntry : NavBackStackEntry = navController.getBackStackEntry(R.id.siteFragment)
makeToast("backStackEntry = ${backStackEntry.destination.label.toString()}")
// Navigate to existing Fragment!! HOW TO DO?
} catch (ex: IllegalArgumentException) {
navController.navigate(R.id.action_contactFragment_to_siteFragment)
// Creates new Fragment as one doesn't existing in backstack. THIS WORKS!
}
}
}
So I have a reference to the Fragment in the BackStack, but I can't see how to navigate to it..
Related
I am trying to implement a way for the menuitem button to navigate to a different fragment based on the current fragment.
For example, if I am in the fragment_home, I can access the dog_fragment and from the dog_fragment I can access another fragment like cat_fragmentand vice versa.
So far I am only able to navigate from fragment_home to dog_fragment, however when I try to access any other fragments from the dog_fragment my app crash. If I try to access another fragment from any other fragment accept from the home_fragment my app crashes
//This is in the Mainactivity
fun onComposeAction(item: MenuItem) {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
val navController = navHostFragment.navController
when (item.getItemId()) {
R.id.Hamster -> {
navController.navigate(R.id.action_home2_to_hamster)
}
R.id.Dog -> {
navController.navigate(R.id.action_home2_to_dog)
}
R.id.Cat ->{
navController.navigate(R.id.action_home2_to_cat)
}
}
}
Simply tie the menuitems to the destination fragments you have.
I currently have an app which uses a One Activity-Many Fragment approach, and within this app is a fragment which shares significant data with its children, and so I have used navGraphViewModels scoped to a nested nav graph as so:
private val viewModel by navGraphViewModels<MySharedViewModel>(R.id.nested_nav_graph)
The parent fragment contains a viewPager, and the fragments passed to that viewPager all share the same viewModel as the parent.
My issue with using this approach is that when it comes to UI testing involving navGraphViewModels using Espresso, I was getting the error "View XXX does not have a NavController set." I managed to fix this for the parent fragment with the below:
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())
UiThreadStatement.runOnUiThread {
navController.setGraph(R.navigation.nested_nav_graph)
}
val scenario =
launchFragmentInContainer(themeResId = R.style.AppTheme) {
MyFragment().also { fragment ->
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
Navigation.setViewNavController(fragment.requireView(), navController)
}
}
}
}
return navController
}
However, as the parent fragment then loads its child fragments into the viewPager and these also require a NavController, my tests don't proceed past the #Before block. Any help regarding how to set the navController to the child fragment would be appreciated.
I have an Activity with a navGraph that conditionally chooses a startDestination when the the Activity is created.
private suspend fun checkStatusAndNavigate(userID: Int) {
val navController = findNavController(nav_host_container)
val user = getUserFromDB(userID)
val dateNow = Date((if (prefSaved) prefTime() else System.currentTimeMillis())
val navGraph = navController.navInflater.inflate(nav_graph).apply {
// NavGraph inflates with one of three possible starting points: A, B, or C.
startDestination = when {
checkDate(dateNow) == DateCheck.OldDate -> R.id.fragment_a
!user.hasOptedIn -> R.id.fragment_b
else -> R.id.fragment_c
}
}
navController.setGraph(navGraph)
}
After navController.setGraph(navGraph), the startDestination is launched. This works as expected.
Fragment B's only destination out is to Fragment C. But when the user is in Fragment C and does onBackPressed(), I would like to skip Fragment B, and return to whatever called the Activity (there are a few options, so overriding onBackPressed isn't a great option).
I don't think this is a popTo or popToInclusive problem. I don't want to clear the backstack. I just want to keep Fragment B from ever entering the backstack if at all possible.
If you don't want B to be accessible on back from C, then you're supposed to create an Action that is popTo=B, popToInclusive=true, and destination=C.
I have a navigation which looks like this
Frag1 -> Frag2 -> Frag3
Inside Frag2 there is a NavHostFragment with its own navigation
InnerFrag1 -> InnerFrag2
If I do this
Navigate to Frag2
Navigate to InnerFrag2 inside Frag2
Navigate to Frag3
Go back
then I'll see InnerFrag2 inside Frag2, when I press back normally I would go from InnerFrag2 to InnerFrag1 inside Frag2 but now it's going to Frag1 instead.
Here is my navigation handling inside Frag2
private val backPressedCallback = OnBackPressedCallback {
navHostFragment.navController.navigateUp()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().addOnBackPressedCallback(backPressedCallback)
}
override fun onDestroyView() {
activity?.removeOnBackPressedCallback(backPressedCallback)
super.onDestroyView()
}
private val navHostFragment: NavHostFragment
get() = childFragmentManager.findFragmentById(R.id.innerNavHostFragment) as NavHostFragment
When going back to Frag2 the fragment in the nav host is the correct one, but navigating back moves away from Frag2 because inner nav host's back stack is lost. Can I persist it somehow or fix it some other way?
EDIT: actually when going from Frag3 to Frag2 I see InnerFrag1 inside, the both look alike, that's why going back at this point brings me back to Frag1
EDIT2: I found my problem, I inflate Frag2s navigation from code in onViewCreated like this
val navHostFragment = (frag2NavHostFragment as? NavHostFragment) ?: return
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
setting it in xml makes it work, I still need to set it from code somehow, Frag2 chooses which navigation to use depending on its arguments
Now my question changes from Navigation's back stack is lost to How to preserve NavHostFragment's state when settings it's graph from code
You can now handle onBackPress on fragments. In your fragment just add this in onViewCreated method.
val navController = Navigation.findNavController(view)
requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navController.popBackStack(R.id.fragmentWhereYouWantToGo, false)
}
})
I would also give a check to app:popUpTo , app:popUpToInclusive or singleTop XML attributes to the fragments inside your Frag2
After looking into this for a little, original question doesn't make much sense, I'd delete it but it got 2 upvotes ¯\_(ツ)_/¯
I solved my problem by adding a check before inflating graph, so that NavHostFragment's graph is set only if it doesn't already have one.
try {
navHostFragment.navController.graph
} catch (e: IllegalStateException) {
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
}
NavController.getGraph doesn't return null, instead it throws IllegalStateException, hence the weird check
I use bottomNavigationView and navigation component. Please tell me how I don't destroy the fragment after switching to another tab and return to the old one? For example I have three tabs - A, B, C. My start tab is A. After I navigate to B, then return A. When I return to tab A, I do not want it to be re-created. How do it? Thanks
As per the open issue, Navigation does not directly support multiple back stacks - i.e., saving the state of stack B when you go back to B from A or C since Fragments do not support multiple back stacks.
As per this comment:
The NavigationAdvancedSample is now available at https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
This sample uses multiple NavHostFragments, one for each bottom navigation tab, to work around the current limitations of the Fragment API in supporting multiple back stacks.
We'll be proceeding with the Fragment API to support multiple back stacks and the Navigation API to plug into it once created, which will remove the need for anything like the NavigationExtensions.kt file. We'll continue to use this issue to track that work.
Therefore you can use the NavigationAdvancedSample approach in your app right now and star the issue so that you get updates for when the underlying issue is resolved and direct support is added to Navigation.
In case you can deal with destroying fragment, but want to save ViewModel, you can scope it into the Navigation Graph:
private val viewModel: FavouritesViewModel by
navGraphViewModels(R.id.mobile_navigation) {
viewModelFactory
}
Read more here
EDIT
As #SpiralDev noted, using Hilt simplifies a bit:
private val viewModel: MainViewModel by
navGraphViewModels(R.id.mobile_navigation) {
defaultViewModelProviderFactory
}
just use navigation component version 2.4.0-alpha01 or above
Update:
Using last version of fragment navigation component, handle fragment states itself. see this sample
Old:
class BaseViewModel : ViewModel() {
val bundleFromFragment = MutableLiveData<Bundle>()
}
class HomeViewModel : BaseViewModel () {
... HomeViewModel logic
}
inside home fragment (tab of bottom navigation)
private var viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.bundleFromFragment.observe(viewLifecycleOwner, Observer {
val message = it.getString("ARGUMENT_MESSAGE", "")
binding.edtName.text = message
})
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.bundleFromFragment.value = bundleOf(
"ARGUMENT_MESSAGE" to binding.edtName.text.toString(),
"SCROLL_POSITION" to binding.scrollable.scrollY
)
}
You can do this pattern for all fragments inside bottom navigation
Update 2021
use version 2.4.0-alpha05 or above.
don't use this answer or other etc.
This can be achieved using Fragment show/hide logic.
private val bottomFragmentMap = hashMapOf<Int, Fragment>()
bottomFragmentMap[0] = FragmentA.newInstance()
bottomFragmentMap[1] = FragmentB.newInstance()
bottomFragmentMap[2] = FragmentC.newInstance()
bottomFragmentMap[3] = FragmentD.newInstance()
private fun loadFragment(fragmentIndex: Int) {
val fragmentTransaction = childFragmentManager.beginTransaction()
val bottomFragment = bottomFragmentMap[fragmentIndex]!!
// first time case. Add to container
if (!bottomFragment.isAdded) {
fragmentTransaction.add(R.id.container, bottomFragment)
}
// hide remaining fragments
for ((key, value) in bottomFragmentMap) {
if (key == fragmentIndex) {
fragmentTransaction.show(value)
} else if (value.isVisible) {
fragmentTransaction.hide(value)
}
}
fragmentTransaction.commit()
}
Declare fragment on the activity & create fragment instance on onCreate method, then pass the fragment instance in updateFragment method. Create as many fragment instances as required corresponding to bottom navigation listener item id.
Fragment fragmentA;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
fragmentA = new Fragment();
updateFragment(fragmentA);
}
public void updateFragment(Fragment fragment) {
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.layoutFragment, fragment);
transaction.commit();
}
Furthermore be sure you are using android.support.v4.app.Fragment and calling getSupportFragmentManager()