Why Android Navigation Component screen not go back to previous Fragment,but a method in onViewCreated of previos Fragment being called? - android

I have 2 fragment call CreateRoomFragment and DisplayPhotoFragment,the navigation graph is look like this:
<navigation>
<fragment
android:id="#+id/createRoomFragment"
android:name="package.room.CreateRoomFragment"
android:label="Create a room"
tools:layout="#layout/fragment_create_room">
<action
android:id="#+id/action_createRoomFragment_to_roomFragment"
app:destination="#id/roomFragment" />
<action
android:id="#+id/action_createRoomFragment_to_displayPhotoFragment"
app:destination="#id/displayPhotoFragment" />
</fragment>
<fragment
android:id="#+id/displayPhotoFragment"
android:name="package.fragment.DisplayPhotoFragment"
android:label="fragment_display_photo"
tools:layout="#layout/fragment_display_photo" >
<argument android:name="bitmap"
app:argType="android.graphics.Bitmap"/>
</fragment>
So when I wanna to move from CreateRoomFragment to DisplayPhotoFragment,I use the do as below:
NavDirections action = CreateRoomFragmentDirections.actionCreateRoomFragmentToDisplayPhotoFragment(selectedPhoto);
Navigation.findNavController(view).navigate(action);
Doing this,I can navigate to DisplayPhotoFragment.
But when I press back button of the device and also the Back arrow from the toolbar,it cant go back to CreateRoomFragment.
I tried this,but still unable to back to previous fragment:
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
navController.navigateUp(); //I tried this
navController.popBackStack(R.id.createRoomFragment,false); //and also this
}
});
Main Problem now:
By using the code above,the screen didnt go back to previous Fragment(CreateRoomFragment).It still stuck in DisplayPhotoFragment,but at the same time,an API method in CreateRoomFragment onViewCreated section is being called.
What causing this? and how can I solve this problem?

I had the same problem. For me the issue was that I was using a LiveData boolean to decide when to go to the next fragment. When I then navigated back/up the boolean was still true so it would automatically navigate forward again.

Android maintains a back stack that contains the destinations you've visited. The first destination of your app is placed on the stack when the user opens the app. Each call to the navigate() method puts another destination on top of the stack. Tapping Up or Back calls the NavController.navigateUp() and NavController.popBackStack() methods, respectively, to remove (or pop) the top destination off of the stack.
NavController.popBackStack() returns a boolean indicating whether it successfully popped back to another destination. The most common case when this returns false is when you manually pop the start destination of your graph.
When the method returns false, NavController.getCurrentDestination() returns null. You are responsible for either navigating to a new destination or handling the pop by calling finish() on your Activity.
When navigating using an action, you can optionally pop additional destinations off of the back stack by using popUpTo and popUpToInclusive parameter of the action.
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (true == conditionForCustomAction) {
CustomActionHere()
} else NavHostFragment.findNavController(this#MyFragment).navigateUp();
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, onBackPressedCallback
)
...
}

The best solution for handling navigation using live data is to use the SingleLiveEvent.
You can always use this class which is an extension of MutableLiveData.
https://gist.githubusercontent.com/cdmunoz/ebe5c4104dadc2a461f512ea1ca71495/raw/a17f76754f86a4c0b1a6b43f5c6e6d179535e627/SingleLiveEvent.kt
For a detail run down of this check:
https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70

Had a similar issue. We still have multiple activities with nav component.
So imagine activity A -> activity B, activity B has its own nav and fragments. When the initial fragment tries to pop the back stack there is nowhere to pop back to and the nav controller does not know to finish the activity. So one solution I found was to do
if (!findNavController().popBackStack()) activity?.finish()
If nav controller can not pop back it will finish activity.

You can use MutableSharedFlow instead on MutableLiveData if you want to observe the Event only once.
in your viewModel:
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() // read-only public view
suspend fun postEvent() {
_events.emit(event) // suspends until subscribers receive it
}
In your Activity/Fragment class:
lifecycleScope.launchWhenStarted {
viewModel.events.collect {
}
}
viewModel.postEvent()
This will prevent observing data continuously when going back to fragment.

Related

Navigation Component - Starting destination is incorrect

I started noticing something in my app the other day and its wildly inconsistent. Sometimes it happens and sometimes it doesn't.
I am using the Navigation Component to handle navigation in the app and I started noticing that sometimes, when popping the backstack via the action bar back button or the device back button, it returns to a fragment that is no longer the starting destination (or at least shouldn't be).
In my case the app starts in MainFragment and once authenticated moves to DashboardFragment. This is a pretty common scenario.
Navigation in the app is pretty flat. most of the time its only 1 level deep so nearly all views are accessible from the dashboard.
The app starts at a login view as many do and then to a dashboard where the session will remain as the "start destination". To accomplish this, its done in the nav_graph using popUpTo and popUpToInclusive.
<fragment
android:id="#+id/mainFragment"
android:name="com.example.view.fragments.MainFragment"
android:label="Welcome">
<action
android:id="#+id/action_mainFragment_to_dashboardFragment"
app:destination="#id/dashboardFragment"
app:popUpTo="#id/mainFragment"
app:popUpToInclusive="true"/>
</fragment>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.example.view.fragments.dashboard.DashboardFragment"
android:label="#string/dashboard_header" >
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment" />
</fragment>
When the user successfully authenticates and its time to go to the dashboard, I use NavController.navigate() to send them there.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment()
)
// This should have the same result and it does appear to be affected by the same issue
// findNavController().navigate(R.id.action_mainFragment_to_dashboardFragment)
I have an action bar with a back arrow and a navigation drawer. In the main activity I need to define the AppBarConfiguration and override onSupportNavigateUp()
lateinit var appBarConfiguration: AppBarConfiguration
...
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate()")
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// There is 2 different drawer menu's respectfully.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.dashboardFragment
), binding.drawerLayout
)
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
...
override fun onSupportNavigateUp(): Boolean {
Timber.d("-> onSupportNavigateUp()")
val breadcrumb = navController
.backStack
.map { it.destination }
.filterNot { it is NavGraph }
.joinToString(" > ") { it.displayName.split('/')[1] }
Timber.d("Backstack: $breadcrumb")
Timber.d("Previous backstack entry: ${navController.previousBackStackEntry?.destination?.displayName}")
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
...
The logs look like this when we step back and it is working correctly
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: dashboardFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/dashboardFragment
D/DashboardFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/DashboardFragment: -> onViewCreated()
I also noticed when using the hamburger in the action bar that it also calls onSupportNavigateUp()
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: pendingAction: false
D/MainActivity: Backstack: dashboardFragment
D/MainActivity: Previous backstack entry: null
When I use the drawer to navigate to a destination I do see this in the logs and im not sure where/why this is returned or if it has any importance
I/NavController: Ignoring popBackStack to destination com.example:id/mainFragment as it was not found on the current back stack
Now, when its NOT working correctly, this is what the logs look like
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: mainFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/mainFragment
D/MainFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/MainFragment: -> onViewCreated()
This really feels like the popUpTo and popUpToInclusive properties are not being applied (sometimes) when performing the navigation from main fragment to dashboard fragment. It's also suspicious that even though the dashboard fragment is not set as the new starting destination but also its missing from the backstack. Assuming the properties were NOT applied I would expected to see the breadcrumb Backstack: mainFragment > dashboardFragment > testingFragment
Any help would be greatly appreciated!
While there may certainly be a better procedure to follow (as outlined in the Principles of Navigation in the previous comments) the overhead of the change introduces a plethora of new errors and the scope is too large at this time.
Its still unknown why popUpTo and popUpToInclusive are not reliable via XML when navigating (even using NavDirections) however, thus far, passing the NavOptions while navigating seems to resolve the issue.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment(),
NavOptions.Builder().setPopUpTo(R.id.mainFragment, true).build()
)
Thus far the issue has yet to arise again.
As it says in Ian's first link:
The back stack always has the start destination of the app at the bottom of the stack.
Your start destination is the "home" one in a navigation graph, the one identified with the little house icon. Set with the startDestination attribute in the XML. When the Navigation library creates a backstack, it always has that destination at the bottom. And it will always be there, even if you try to avoid it with the poUpTo attributes.
That's why if there's a fragment you consider your "home" one, like your dashboard, that needs to explicitly be your startDestination. It's the last thing a user will see if they back out of your app. If you have something like a login or welcome screen as the start destination, they'll back out to that.
Which is why you need to set the "home" fragment as the start destination, and then handle any extra navigation to a login or welcome screen from there. It's just how the Navigation stuff is designed to work. If you try and work around it (I did!) you'll run into other problems, and a lot of the nice features like automatic backstack recreation might not work properly

OnBackPress On Fragment

I am working on android application that contains five fragment on an activity, What I want is as the fragment 1 is opened and I back-press it comes to Main fragment and same as I press back-press from fragment 5 it also comes to Main fragment.
and When I press on Backpress from MainFragment, the App should Exit.
I have Gone through this link Link
and I have also added the Dispatcher but It not met my requirement.
Like I am always opening each fragment like this
private fun ShowQRCodeFragment() {
val newFragment: Fragment = QrCodeScanningFragment()
val transaction1: FragmentTransaction = supportFragmentManager.beginTransaction()
transaction1.replace(R.id.frameLayout, newFragment)
transaction1.addToBackStack(null)
transaction1.commit()
}
Updated the transaction
private fun FunctionNewSettings() {
val newFragment: Fragment = CustomSettingsFragment()
val transaction1: FragmentTransaction = supportFragmentManager.beginTransaction()
transaction1.replace(R.id.frameLayout, newFragment)
transaction1.addToBackStack("namedata")
fragmentManager.popBackStack()
transaction1.commit()
}
You should use addToBackStack() while fragment transaction. This will allow you to go to the previous fragment on back-press.
For the app exit case, check if the current fragment is MainFragment with the help of fragment tag and calling fragmentmanager.popBackStack() or super.onBackPressed() accordingly.
In MainFragment, use
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(
true // default to enabled
) {
override fun handleOnBackPressed() {
requireActivity().finish()
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, // LifecycleOwner
callback
)
}
In another fragments, use
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(
true // default to enabled
) {
override fun handleOnBackPressed() {
for (i in 0 until (requireActivity() as FragmentActivity).supportFragmentManager.backStackEntryCount) {
activity.supportFragmentManager.popBackStack()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, // LifecycleOwner
callback
)
}
if u want to go back to the previous fragment first use
addToBackStack()
and if you want to exit the app/activity by using onBackPressed from activity then in MainFragment use
getActivity().onBackPressed();
if you want to finish the activity from Fragment use
getActivity().finish();
You can also replace existing fragment when user clicks Back button using
fragmentTransaction.replace(int containerViewId, Fragment fragment, String tag)
Solution
Override onBackPressed() method inside your activity.
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count > 1) {
repeat(count - 1) { supportFragmentManager.popBackStack() }
} else {
finish()
}
}
You don't need to mess around with the back button behaviour if you're just switching fragments around, and you shouldn't need to pop the backstack either.
The backstack is just a history, like the back button on your browser. You start with some initial state, like an empty container layout. There's no history before this (nothing on the backstack), so if you hit back now, it will back out of the Activity completely.
If you start a fragment transaction where you add a new fragment to that container, you can use addToBackStack to create a new "step" in the history. So it becomes
empty container -> added fragment
and if you hit back it takes a step back (pops the most recent state off the stack)
empty container
if you don't use addToBackStack, the change replaces the current state on the top of the stack
(with addToBackStack)
empty container -> fragmentA -> fragmentB
(without it)
empty container -> fragmentB
so usually you'll skip adding to the backstack when you add your first fragment, since you don't want an earlier step with the empty container - you want to replace that state
empty container
(add mainFragment without adding the transaction to the backstack)
mainFragment
and now when you're at that first state showing mainFragment, the back button will back out of the activity
So addToBackStack makes changes that are added to the history, and you can step back through each change. Skipping it basically alters the last change instead of making a new one. You can think of it like adding to the backstack is going down a level, so when you hit back you go back up to the previous level. Skipping the add keeps you on the same level, and just changes what you're looking at - hitting back still takes you up a level.
So you can use this to organise the "path" the back button takes, by adding new steps to the stack or changing the current one. If you can write out the stack you want, where the back button takes you back a step each time, you can create it!
One last thing - addToBackStack takes a String? argument, which is usually null, but you can pass in a label for the step you're adding. This allows you to pop the backstack all the way back to a certain point in the history, which is like when a browser lets you jump to the previous site in the history, and not just the last page.
So you can add a name for the transaction, like "show subfragment" when you're adding your first subfragment on top of mainFragment, meaning you can use popBackstack with that label to jump straight to the initial mainFragment state, where the next back press exits the activity. This is way more convenient than popping each step off the backstack, and keeping track of how many you need to do - you can just jump back in the history to a defined point

Problem going back from current Fragment back to the replaced Fragment when using NavHostFragment

I have three fragments A, B and C. And I'm using navHostFragment container in MainActivity. So the application goes from A -> B using kotlin extension function findNavController().navigate... and then go from B to C using same function. All works fine till here.
Now in Fragment C, I'm replacing different elements on fragment C using
activity?.supportFragmentManager
?.beginTransaction()
?.replace(R.id.list_container, someFragment)
?.addToBackStack("some_frag_id")
?.commit()
The list_container is replaced with someFragment. After this when I press physical back button Fragment C pops out and my app goes to Fragment B while what I expect it to restore replaced list_container i.e. whatever was there before replacement.
I'm also overiding this in my MainActivity
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count == 0) {
super.onBackPressed()
//additional code
}
else {
supportFragmentManager.popBackStack()
}
}
I'm not sure what is missing here. I have read a lot of solutions on stackoverflow but none worked to my satisfaction. Please guide.
If you are adding a Fragment to a View within a Fragment, you must always use the childFragmentManager - using activity?.supportFragmentManager is always the wrong FragmentManager to use in that case.
Besides fixing cases with restoring state (which would not work when using the wrong FragmentManager), this also ensures that the default behavior for dispatching onBackPressed() down the FragmentManager hierarchy will work out the box - you should not need any logic at all in onBackPressed() to have the pop work correctly.
If you need to intercept the back button in Fragment C, you should follow the providing custom back documentation to register an OnBackPressedDispatcher - you should not override onBackPressed() even in those cases.

Android navigation component with popupTo and popUpToInclusive onResume not working

I'm trying to figure out how the navigation component works. Here's my fragments, SignIn Fragment and HomeFragment. In my navigation graph I have the following
<fragment
android:id="#+id/signInFragment"
android:name="com.nitsua.chatapp.screen.authentication.SignInFragment"
android:label="SignInFragment"
tools:layout="#layout/fragment_signin">
<action
android:id="#+id/navigate_to_home"
app:destination="#id/homeFragment"
app:popUpTo="#id/signInFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/homeFragment"
android:name="com.nitsua.chatapp.screen.home.HomeFragment"
android:label="HomeFragment"
tools:layout="#layout/fragment_home"/>
If I understand is correct, when I use the action: navigate_to_home, it will clear the fragment backstack until signInFragment including it because of popUpToInclusive set to true. If it is not set to true. SignInFragment will not be removed in backstack.
I tried signInFragment -> homeFragment, then homeFragment and back button. It works fine. It doesn't go to sign in and the app goes out of foreground. Then I try to open again the app from background. I expect the app to be in HomeFragment but it is in SignInFragment. The fragment backstack should store the HomeFragment transaction, is that right? then when it resume, it should resume the HomeFragment. but why it resumed the SignInFragment? Am I missing something? Can someone explain to me and recommend a way to resume to HomeFragment instead on SignInFragment?
Thank you.
You could put the signInFragment as an Activity. Check in your MainActivity if you have to show the signInActivity. If yes then start the activity.
Or you could start your homeFragment and right after that start the signInFragment if log in is required.
Then use this in your signInFragment.
navController.popBackStack(R.id.homeFragment, false)
Will pop your backstack till it reaches the first instance of homeFragment.
I ran into problem when start using navigation component but unfortunately Navigation component not provide any solution yet to call onResume when backStack or popup to another fragment .It reCreate the backfragment again when press back button.
My solution:
My data is in repository (Either getting from local Db or remote ).
Then create a LiveData in ViewModel and change his value false to true when first time load data from Api or Database .After then when user press back button check from liveData if value is true then get data from viewmodel and not calling the api.
private val localOrRemoteData=MutableLiveData()
fun setLocalOrRemoteData(status:Boolean){
localOrRemoteData.value=status
}
fun getLocalOrRemoteData():MutableLiveData<Boolean>{
return localOrRemoteData
}
getLocalOrRemoteData().observeOnce(lifecycleOwner) {
temp = it
}

Navigation Component .popBackStack() with arguments

I have Two fragment. SecondFragment and ThirdFragment. Actually I use the Navigation Component for passing value between fragments. Like this:
SecondFragment:
val action = SecondFragmentDirections.action_secondFragment_to_thirdFragment().setValue(1)
Navigation.findNavController(it).navigate(action)
Here is how I read the value from the ThirdFragment:
arguments?.let {
val args = ThirdFragmentArgs.fromBundle(it)
thirdTextView.text = args.value.toString()
}
It's work fine. Now my stack is look like this:
ThirdFragment
SecondFragment
There is any option for pass value from the opened ThirdFragment to the previous SecondFragment with the new Navigation Component? (When ThirdFragment is finishing)
I know about onActivityResult, but If Nav.Component serve better solution than I want use that.
Thank you!
It's a bit late for this answer but someone may find it useful. In the updated versions of the navigation component library it is now possible to pass data while navigating back.
Suppose the stack is like this
FragmentA --> FragmentB.
We are currently now in FragmentB and we want to pass data when we go back to FragmentA.
Inside FragmentAwe can create an observer with a key:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
// Instead of String any types of data can be used
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")
?.observe(viewLifecycleOwner) {
}
}
Then inside FragmentB if we change its value by accessing previous back stack entry it will be propagated to FragmentA and observer will be notified.
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("key", "value that needs to be passed")
navController.popBackStack()
Just came across setFragmentResult(), pretty easy to use. The docs on this are here.
If you are navigating: Fragment A -> Fragment B -> Fragment A
Add this to fragment A:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("requestKey") { requestKey, bundle ->
shouldUpdate = bundle.getBoolean("bundleKey")
}
}
Then in fragment B add this line of code:
setFragmentResult("requestKey", bundleOf("bundleKey" to "value to pass back"))
// navigate back toFragment A
When you navigate back to fragment A the listener will trigger and you'll be able to get the data in the bundle out.
What you are asking for is an anti-pattern. You should either
navigate to the second fragment again with the new values you would like to set
use the third fragment ins a separate activity and start it with startActivityForResult()
use a ViewModel or some kind of singleton pattern to hold on to your data (make sure you clear the data after you no longer need it)
these are some of the patterns that came to my mind. Hope it helps.
As described here:
When navigating using an action, you can optionally pop additional destinations off of the back stack. For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated element. app:popUpTo tells the Navigation library to pop some destinations off of the back stack as part of the call to navigate(). The attribute value is the ID of the most recent destination that should remain on the stack.
<fragment
android:id="#+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="#layout/fragment_c">
<action
android:id="#+id/action_c_to_a"
app:destination="#id/a"
app:popUpTo="#+id/a"
app:popUpToInclusive="true"/>
</fragment>

Categories

Resources