Navigation's back stack is lost when navigation is nested - android

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

Related

Issue with backstack and bottomnav in kotlin

I have a bottom nav with 4 fragments Home, Following, Notification, and Profile, there is no issue with the bottom navigation on backstack , but now for eg from profile fragment I jumped to a fragment called edit_profile which is not a part of the bottom nav and when press back I want that it should go back to the profile fragment but the backstack is taking me from edit_profile to directly home fragment
here is a recording link
I recently change my project from java to kotlin and I'm a beginner in kotlin
i really like the navigation of Pinterest and Instagram
Note:- All this code is automatically changed to kotlin (with some
changes done manually ) , this issue was also with java and not after migrating to kotlin , Also if you want more reference of the code
please tell me i will update the question
Code
MainActivity.kt // Bottom Nav
class MainActivity : AppCompatActivity() {
var bottomNavigationView: BottomNavigationView? = null
var integerDeque: Deque<Int> = ArrayDeque(3)
var flag = true
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
setContentView(R.layout.activity_main)
val window = this.window
window.statusBarColor = this.resources.getColor(R.color.black)
bottomNavigationView = findViewById(R.id.bottom_navigation_view)
integerDeque.push(R.id.nav_home)
loadFragments(Home_Fragment())
bottomNavigationView!!.selectedItemId = R.id.nav_home
bottomNavigationView!!.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem ->
val id = item.itemId
if (integerDeque.contains(id)) {
if (id == R.id.nav_home) {
integerDeque.size
if (flag) {
integerDeque.addFirst(R.id.nav_home)
flag = false
}
}
integerDeque.remove(id)
}
integerDeque.push(id)
loadFragments(getFragment(item.itemId))
false
}
)
}
#SuppressLint("NonConstantResourceId")
private fun getFragment(itemId: Int): Fragment {
when (itemId) {
R.id.nav_home -> {
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
R.id.nav_following -> {
bottomNavigationView!!.menu.getItem(1).isChecked = true
return Following_Fragment()
}
R.id.nav_notification -> {
bottomNavigationView!!.menu.getItem(2).isChecked = true
return Notification_Fragment()
}
R.id.nav_profile -> {
bottomNavigationView!!.menu.getItem(3).isChecked = true
return Profile_Fragment()
}
}
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
private fun loadFragments(fragment: Fragment?) {
if (fragment != null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment, fragment.javaClass.simpleName)
.commit()
}
}
override fun onBackPressed() {
integerDeque.pop()
if (!integerDeque.isEmpty()) {
loadFragments(getFragment(integerDeque.peek()))
} else {
finish()
}
}
Edit_Profile.kt // from this fragment i want to go back to the last fragment which should be the profile fragment
class Edit_Profile : Fragment() {
private var profilePhoto: CircleImageView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_edit_profile, container, false)
profilePhoto = view.findViewById(R.id.circleImageView)
initImageLoader()
setProfileImage()
val imageView = view.findViewById<ImageView>(R.id.backArrow)
imageView.setOnClickListener {
val newCase: Fragment = Profile_Fragment()
assert(fragmentManager != null)
val transaction = requireFragmentManager().beginTransaction()
transaction.replace(R.id.fragment_container, newCase)
transaction.addToBackStack(Profile_Fragment.toString())
transaction.commit()
}
return view
}
Edit
added a part of the transaction from Profile Fragment to Edit Profile
ProfileFragment.kt
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
})
Now you are managing the back stack through the integerDeque array.
When you go to a new BottomNavigationView fragment; you added its id to the array if it doesn't already exist.
When you pop up the back stack; the fragment at the top is kicked off the array.
But since you pushed all those ids in the bottomNavigationView.setOnItemSelectedListener callback; then the integerDeque array only contains BottomNavigationView fragments ids.
And as the Edit_Profile fragment is not a part of BottomNavigationView fragments, then it won't be added/popped off the queue. Instead when you try to popup the back stack whenever the Edit_Profile fragment is shown; the normal behavior you manage in the onBackPressed() continues and the Profile_Fragment id will pop up from the queue making you return to the preceding fragment (Home_Fragment) in your mentioned example.
A little fix to this is to consider adding an id into the queue when you transact to Edit_Profile fragment so that this id is popped off the queue resulting in back to Profile_Fragment fragment.
You can do that with the fragment's id in order to make sure it's unique:
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
(requireActivity() as MainActivity).integerDeque.push(id) // <<<< pushing id to the queue
})
This should fix your problem.
Side tips:
Use setOnItemSelectedListener instead of setOnNavigationItemSelectedListener on the BNV as the latter is deprecated.
Return true instead of false from setOnItemSelectedListener callback as this should consume the event and mark the BNV as selected.
In Edit_Profile transaction replace the fragment instead of adding it with add as already the container is consumed; and this would make you avoid overlapping fragments in the container.
In onBackPressed(); you'd replace loadFragments(..) with bottomNavigationView.selectedItemId = integerDeque.peek(); this could be lighter to reuse the same fragment instead of redoing the transaction.
Usually I follow this pattern
Where I add HomeF in main container which includes all bottom nav tab, and all bottom nav tab will open in home container, and those fragment which are not part of bottom nav will open in main container. I generally add(not replace) all the fragments in main container and set add to back stack , so that if user goes from profile (home_container) to something in main container , while backstack we can pop the top fragment and user will be seeing profile.

Fragments not added to backstack using Navigation Components

Information:
I'm programmatically inserting a NavHostFragment for each feature of the app. Each NavHostFragment has it's own Navigation Graph. Dagger is providing them by using a FragmentFactory specific to each feature. It's a Single Activity structure with MVVM architecture.
Repo: https://github.com/mitchtabian/DaggerMultiFeature/tree/nav-component-backstack-bug
checkout the branch "nav-component-backstack-bug".
The Problem
When navigating into the graph the fragments are not being added to the backstack. The only fragment that's added is whichever one has most recently been visited. So the stack size always stays at one.
Originally I thought it was because I wasn't setting the FragmentFactory to the ChildFragmentManager but that doesn't change anything. See the code snippets below for the relevant code. Or checkout the project and run it. I have logs printing out the fragments currently in the backstack from the ChildFragmentManager and also the SupportFragmentManager. Both have a constant size of 1.
Feature1NavHostFragment.kt
This is one of the custom NavHostFragment's. The create() function in the companion object is how I create them.
class Feature1NavHostFragment
#Inject
constructor(
private val feature1FragmentFactory: Feature1FragmentFactory
): NavHostFragment(){
override fun onAttach(context: Context) {
((activity?.application) as BaseApplication)
.getAppComponent()
.feature1Component()
.create()
.inject(this)
childFragmentManager.fragmentFactory = feature1FragmentFactory
super.onAttach(context)
}
companion object{
const val KEY_GRAPH_ID = "android-support-nav:fragment:graphId"
#JvmStatic
fun create(
feature1FragmentFactory: Feature1FragmentFactory,
#NavigationRes graphId: Int = 0
): Feature1NavHostFragment{
var bundle: Bundle? = null
if(graphId != 0){
bundle = Bundle()
bundle.putInt(KEY_GRAPH_ID, graphId)
}
val result = Feature1NavHostFragment(feature1FragmentFactory)
if(bundle != null){
result.arguments = bundle
}
return result
}
}
}
MainActivity.kt
This is an example of how I create the NavHostFragment's in MainActivity.
val newNavHostFragment = Feature1NavHostFragment.create(
getFeature1FragmentFactory(),
graphId
)
supportFragmentManager.beginTransaction()
.replace(
R.id.main_nav_host_container,
newNavHostFragment,
getString(R.string.NavHostFragmentTag)
)
.setPrimaryNavigationFragment(newNavHostFragment)
.commit()
Feature1MainFragment.kt
And here is an example of how I'm navigating to other fragments in the graph.
btn_go_next.setOnClickListener {
findNavController().navigate(R.id.action_feature1MainFragment_to_feature1NextFragment)
}
Summary
Like I said, in every fragment I'm printing the backstack for both the ChildFragmentManager and the SupportFragmentManager. Both have a constant size of one. It's as if the fragments are being replaced as I navigate into the graph instead of added to the stack.
Anyways, thanks for reading this and I would appreciate any insights.
Looks like a misunderstanding on my part (and a bug, also on my part).
If you loop through the fragments in the childFragmentManager it only ever shows the top-most fragment for some reason.
Example
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val fragments = navHostFragment.childFragmentManager.fragments
for(fragment in fragments){
// Only prints a single fragment, no matter the backstack size
}
However, if you print the backstack size like this, you will get the correct answer.
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val backstackCount = navHostFragment.childFragmentManager.backStackEntryCount
println("backstack count: $backstackCount")
At the end of the day this misunderstanding caused me to believe the fragments were not being added to the backstack. All is good.

How to change the defaultNavHost among a few already created fragments?

I have a couple of fragments in my activity, each having their own Navigation Graph, they're created when the activity is created. What I want to know is how can I choose any of them to be the defaultNavHost or the PrimaryNavigationFragment after they are created?
The code below creates them and sets them as the default one, but I don't want to recreate them each time. How can I do that?
val finalHost = NavHostFragment.create(navigation)
supportFragmentManager.beginTransaction()
.replace(R.id.navFragment0, finalHost)
.setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
.commit()
Couldn't find the code to do what I wanted, so here's my workaround:
First remove the #setPrimaryNavigationFragment(finalHost) from fragmentManager's transaction in your Code, and/or remove app:defaultNavHost="true" from the <fragment...> in your XML.
Then create your NavHostFragments and add each of them to a HashMap:
private val navHostFragments = HashMap<Int, NavHostFragment?>()
fun createNavHosts() {
val finalHost: NavHostFragment =
NavHostFragment.create(navigationId)
.also { navHostFragments[aHandleToTheNavHost] = it }
}
Finally in onBackPressed get the navController of the proper navHost and navigate up on it:
override fun onBackPressed() {
if (navHostFragments[aHandleToTheNavHost]?.navController?.navigateUp() == false)
super.onBackPressed()
}
Note: when #navigateUp() returns false, it means there was nowhere to navigate up to. So in there you can finish the activity or anything else you may want to do.

Android navigation component: how save fragment state

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()

Navigate to fragment on FAB click (Navigation Architecture Components)

I have no idea how to, using the new navigation architecture component, navigate from my main screen (with a FloatingActionButton attatched to a BottomAppBar) to another screen without the app bar.
When I click the fab I want my next screen (fragment?) to slide in from the right. The problem is where do I put my BottomAppBar? If I put it in my MainActivity then I have the issue of the FloatingActionButton not having a NavController set. I also cannot put my BottomAppBar in my Fragment. I am at a loss.
Ran into this issue today and I found out that there is a simple and elegant solution for it.
val navController = findNavController(R.id.navHostFragment)
fabAdd.setOnClickListener {
navController.navigate(R.id.yourFragment)
}
This takes care of the navigation. Then you must control the visibility of your BottomAppBar inside your Activity.
You could have your BottomAppBar in MainActivity and access your FloatingActionButton in your fragment as follows
activity?.fab?.setOnClickListener {
/*...*/
findNavController().navigate(R.id.action_firstFragment_to_secondFragment, mDataBundle)
}
You could hide the BottomAppBar from another activity as follows
(activity as AppCompatActivity).supportActionBar?.hide()
Make sure you .show() the BottomAppBar while returning to previous fragment
Put it in MainActivity and setOnClickListener in onStart() of the activity and it will work fine.
override fun onStart() {
super.onStart()
floatingActionButton.setOnClickListener {
it.findNavController().navigate(R.id.yourFragment)
}
}
Note:This solution is like and hack and better is to follow Activity LifeCycle and setUp OnClickListener when the activity is ready to interact.
Similar question [SOLVED]
if you wanted to navigate to certain fragment (not the star one) in the beginning for some reason, and also you have to graphs for one activity, here is what I suggest:
this method will start activity
companion object {
const val REQUEST_OR_CONFIRM = "request_or_confirm"
const val IS_JUST_VIEW = "IS_JUST_VIEW"
const val MODEL = "model"
fun open(activity: Activity, isRequestOrConfirm: Boolean, isJustView: Boolean = false, model: DataModel? = null) {
val intent = Intent(activity, HostActivity::class.java)
intent.putExtra(REQUEST_OR_CONFIRM, isRequestOrConfirm)
intent.putExtra(IS_JUST_VIEW, isJustView)
intent.putExtra(MODEL, model)
activity.startActivity(intent)
}
}
and then in, onCreate method of Host Activity, first decide which graph to use and then pass the intent extras bundle so the start fragment can decide what to do:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_purchase_nav)
if (intent.getBooleanExtra(REQUEST_OR_CONFIRM, true)) {
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_first_scenario, intent.extras)
} else {
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_second_scenario, intent.extras)
}
}
and here's how you can decide what to do in start fragment:
if (arguments != null && arguments!!.getBoolean(HostActivity.IS_JUST_VIEW)){
navigateToYourDestinationFrag(arguments!!.getParcelable<DataModel>(HostActivity.MODEL))
}
and then navigate like you would do normally:
private fun navigateToYourDestinationFrag(model: DataModel) {
val action = StartFragmentDirections.actionStartFragmentToOtherFragment(model)
findNavController().navigate(action)
}
here's how your graph might look in case you wanted to jump to the third fragment in the beginning
PS: make sure you will handle back button on the third fragment, here's a solution
UPDATE:
as EpicPandaForce mentioned, you can also start activities using Navigation Components:
to do that, first add the Activity to your existing graph, either by the + icon (which didn't work for me) or by manually adding in the xml:
<activity
android:id="#+id/secondActivity"
tools:layout="#layout/activity_second"
android:name="com.amin.SecondActivity" >
</activity>
you can also add arguments and use them just like you would in a fragment, with navArgs()
<activity
android:id="#+id/secondActivity"
tools:layout="#layout/activity_second"
android:name="com.amin.SecondActivity" >
<argument
android:name="testArgument"
app:argType="string"
android:defaultValue="helloWorld" />
</activity>
in koltin,here's how you would use the argument, First declare args with the type of generated class named after you activity, in this case SecondActivityArgs in top of your activity class:
val args: SecondActivityArgsby by navArgs()
and then you can use it like this:
print(args.testArgument)
This doesn't destroy BottomAppBar. Add this to MainActivity only and don't do anything else
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
fabAdd.setOnClickListener {
findNavController(navHostFragment).navigate(R.id.fab)
}

Categories

Resources