I'm using navigation componetns with drawer and app bar in my program.
From home fragment, it has a recyclerview, and when each item is clicked, the nav_host_fragment host another fragment with the selected itme, using the following line:
Navigation.findNavController(view).navigate(R.id.doTimedTaskFragment, args);
The problem, is, I need to ask user if they want to give up the progress they made in the new fragment (doTimedTaskFragment) if they hit the back button in app bar.
I digged Google Document and in the following link gives how I should do it.
https://developer.android.com/guide/navigation/navigation-custom-back#java
public class MyFragment extends Fragment {
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This callback will only be called when MyFragment is at least Started.
OnBackPressedCallback callback = new OnBackPressedCallback(true /* enabled by default */) {
#Override
public void handleOnBackPressed() {
// Handle the back button event
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
// The callback can be enabled or disabled here or in handleOnBackPressed()
}
...
}
However, handleOnBackPressed() never gets called, and I'm suspecting the line of code I've used above to direct to the new fragment, and I can't find a way to resolve this issue.
EDIT/UPDATE:
My question was originally asking how to handle clicking Navigation component left(back) arrow click behavior, and in my case, I overrodeonOptionsItemSelected inside the fragment and code came to be way much cleaner and better than overriding inside MainActivity.
Here's screenshot of my mobile navigation.xml
The OnBackPressedCallback is called for back button on the device not the Toolbar back button. For the toolbar back button you have to set it up with the navigation controller.
In your activity's onCreate:
setSupportActionBar(findViewById(R.id.toolbar))
val nc = findNavController(this, R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration.Builder(nc.graph).build()
setupActionBarWithNavController(
this,
nc,
appBarConfiguration
)
and then:
override fun onSupportNavigateUp(): Boolean {
//Handle the toolbar back button here.
val navController = findNavController(this, R.id.nav_host_fragment)
navController.currentDestination?.let {
if (it.id == R.id.someFragment) {
//do something here
} else {
navController.popBackStack()
}
}
return super.onSupportNavigateUp()
}
Related
I have a single activity app that uses navigation graph and a navigation drawer to go to some of the fragments. Pressing back from each of the fragments usually brings me back to the main fragment, UNLESS I turn the screen off and back on or I put the app in the background. When I resume the app, the up button widget turns back into a hamburger menu, but navigation doesn't happen. Pressing the android back button doesn't navigate either, as if the app forgets where to navigate to.
val navController = (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
setSupportActionBar(layoutAppBarMain.layoutToolbarMain)
NavigationUI.setupActionBarWithNavController(this#MainActivity, navController, mainDrawerLayout)
appBarConfiguration = AppBarConfiguration(navController.graph, mainDrawerLayout)
NavigationUI.setupWithNavController(mainActivityNavView, navController)
supportActionBar?.setDisplayShowTitleEnabled(false)
navController.addOnDestinationChangedListener { _: NavController, nd: NavDestination, _: Bundle? ->
when (nd.id) {
R.id.playFragment -> mainDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
R.id.navRulesFragment, R.id.navImproveFragment, R.id.navAboutFragment, R.id.navDonateFragment -> mainDrawerLayout.setDrawerLockMode(
DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
else -> {
binding.layoutAppBarMain.layoutToolbarMain.navigationIcon = null
mainDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
}
}
Then overriding the onSupportNavigateUp():
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp()
}
If you don't have below code just add and try again:
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null) || super.onSupportNavigateUp()
}
Edit: If that doesn't work then try this:
layoutAppBarMain.layoutToolbarMain.setNavigationOnClickListener { onBackPressed() }
I found out what was causing it. I was using postponeEnterTransition() to check if the database was empty. If it is, then it should load the first fragment, else the second fragment. The problem was that I was using startPostponedEnterTransition() after the navigation had already left the first fragment, which was causing the navController to misbehave.
To solve this, I am now starting the enter transition in the first fragment, then hiding it, before navigating to the second fragment. I am also using a splash screen which is being turned off after the navigation is complete.
In MainActivity:
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { mainVm.keepSplashScreen.value }
super.onCreate(savedInstanceState)
In onCreateView of the first fragment:
postponeEnterTransition() // Wait for data to load before displaying fragment
mainVm.matchState.observe(viewLifecycleOwner, EventObserver { matchState ->
when (matchState) {
RULES_IDLE -> mainVm.transitionToFragment(this, 0)
// Here, if database is not empty,
// start transition right away and hide the view only then navigate
// If the transition happens after the navigation has started,
// the navController will have issues returning from the navDrawer fragments.
GAME_IN_PROGRESS -> {
startPostponedEnterTransition()
view?.visibility = GONE
navigate(RulesFragmentDirections.gameFrag()
}
else -> Timber.e("No implementation for state $matchState at this point")
}
})
Within the MainViewModel:
private val _keepSplashScreen = MutableStateFlow(true)
val keepSplashScreen = _keepSplashScreen.asStateFlow()
fun transitionToFragment(fragment: Fragment, ms: Long) = viewModelScope.launch {
fragment.startPostponedEnterTransition()
delay(ms) // Add a delay for the content to fully load before turning off the splash screen
_keepSplashScreen.value = false
}
Aside from this, the navController implementation works correctly as per the post above.
I am using onSupportNavigateUp() in activity and now i am moving to use single activity architecture and navigation component problem is that i stuck here i don't find any elternative to onSupportNavigateUp() and onBackPressed() in fragment and navcontroller
override fun onSupportNavigateUp(): Boolean {
// some code like showing ad
onBackPressed()
return super.onSupportNavigateUp()
}
but i find solution solution onBackPressed() with onBackPressedDispatcher but problem is that is only work when navigation bar back button pressed not for toolbar up button
requireActivity().onBackPressedDispatcher.addCallback(this) {
// some code like showing ad
}
for me, that work is you have set up navigation component with toolbar like this
toolbar.setupWithNavController(findNavController())
toolbar.setNavigationOnClickListener {
// some code
findNavController().navigateUp()
}
I have a fragment A which sends a search query to the network, and if the result is positive uses Android navigation component to navigate to fragment B, and its done using observers.
After navigation to fragment B, i click on "<-" arrow on the top of the screen, but instead of navigating back to fragment A it reloads fragment B again. And if using the native "back" button on the device, the app crashes with "illegalArgumentException navigation destination unknown" error.
I check the internet for clues on this issue, but all i learned is that this happens because i am using .observe in onViewCreated() and when i go back, it gets called again, and because livedata has something in it already, it just navigates me back to B.
I have tried observing in onActivityCreated(), and using getViewLifeCycleOwner, but no success... the only thing that helped is checking if livedata has observers and returning if true, before using .observe, but it seems incorrect.
This is the viewModel:
private val getAssetResult = MutableLiveData<GeneralResponse<Asset>>()
private val updateAssetResult = MutableLiveData<GeneralResponse<Int>>()
private val deleteAssetResult = MutableLiveData<GeneralResponse<Int>>()
init {
state.value = ViewState(false)
Log.d(TAG, "State in init: $state")
}
fun getAssetResult(): LiveData<GeneralResponse<Asset>>{
return getAssetResult
}
fun findAsset(req: GetAssetRequest) {
scope.launch {
setProgressIndicator(true)
val result = repository.getAsset(req)
getAssetResult.postValue(result)
setProgressIndicator(false)
}
}
This is the fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(EditAssetViewModel::class.java)
setupViewModel()
initFields()
}
private fun setupViewModel() {
if (viewModel.getAssetResult().hasObservers()) // <- This is the part that prevents the app from crashing.
return
viewModel.getAssetResult().observe(this, Observer {
if (it == null) return#Observer
handleSearchResult(it)
})
if (viewModel.getState().hasObservers())
return
viewModel.getState().observe(this, Observer { handleState(it) })
}
private fun handleSearchResult(response: GeneralResponse<Asset>) {
if (response.singleValue == null) {
Toast.makeText(context!!, response.errorMessage, Toast.LENGTH_SHORT).show()
return
}
targetFragment?.let { it ->
val bundle = bundleOf("asset" to response.singleValue)
when(it) {
"UpdateLocation" ->
Navigation.findNavController(view!!).navigate(R.id.updateLocation, bundle)
"EditAsset" -> {
Navigation.findNavController(view!!).navigate(R.id.editAsset, bundle)
}
}
}
}
if i remove this part from the setupViewModel function:
if (viewModel.getAssetResult().hasObservers())
return
the app will either crash when clicked "back" using the device button or go back to fragment A, just to be navigated back to fragment B because of the .observe function.
Override the method onBackPressed() to handle the "<-" arrow
Seems like the LiveData that you use to signal to fragment A that it should navigate to fragment B is actually an event. An event happens only once and once it is consumed (navigation event is done), it is gone. Therefore, after navigating you need to send a message to the viewmodel that the navigation took place and that the corresponding data holder should be (e.g.) null again. In Fragment A you check that the new value is unequal to null, and only if this is the case, you issue the navigation event. This would prevent fragment A to immediatelly switch to B again in the back scenario.
If you want to learn more about ways to use live data for events, please refer to this article.
I recently updated my dependencies to include the OnBackPressedCallback change from an interface into an abstract class.
I have set things up according to the new documentation here but I feel like things are not working as they should.
My fragment's OnCreate looks a lot like the documentation:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this) {
backPressed()
}
}
When I press the back button, the code in backPressed() is run, but nothing more happens.
I have tried calling handleBackPressed() and requireActivity().onBackPressedDispatcher.onBackPressed() and requireActivity().onBackPressed() from inside the callback, but those all cause a StackOverflowError because it seems to run that callback recursively.
There has got to be something really obvious I am missing...
There has got to be something really obvious I am missing...
You forget to disable your custom callback in you fragment before asking Activity to handle back pressed.
My solutiuon suitable for me:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final OnBackPressedCallback callback = new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
if (/*situation to handle back pressing*/){
//here handle your backPress in your fragment
} else {
setEnabled(false); //this is important line
requireActivity().onBackPressed();
}
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
}
When you register an OnBackPressedCallback, you are taking on the responsibility for handling the back button. That means that no other on back pressed behavior is going to occur when you get a callback.
If you're using Navigation, you can use your NavController to pop the back stack:
requireActivity().onBackPressedDispatcher.addCallback(this) {
backPressed()
// Now actually go back
findNavController().popBackStack()
}
This works for me in androidx.appcompat:appcompat:1.1.0
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d(TAG, "Fragment back pressed invoked")
// Do custom work here
// if you want onBackPressed() to be called as normal afterwards
if (isEnabled) {
isEnabled = false
requireActivity().onBackPressed()
}
}
}
)
You can also remove callback instead of setting enabled if it's no longer needed. I use it with nested graph like this because when you touch back in a nested nav graph with it's NavHostFragment, it removes it from main fragment back stack instead of opening last fragment in nested nav graph.
// Get NavHostFragment
val navHostFragment =
childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment)
// ChildFragmentManager of the current NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount
if (backStackEntryCount == 1) {
// We are at the root of nested navigation, remove this callback
remove()
requireActivity().onBackPressed()
} else {
navController?.navigateUp()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
I want to add custom up navigation from fragment using Navigation component
In my build.gradle(app) I use androidx.appcompat:appcompat:1.1.0-alpha04 dependency to have access to onBackPressedDispatcher from activity.
So I implemented OnBackPressedCallback in my fragment and
registered callback to dispatcher:
requireActivity().onBackPressedDispatcher.addCallback(this)
I expected that pressing navigate up in toolbar will call it, but it doesn't.
Pressing device's back button calls it as expected.
Is there a similar way to add some callback in fragment on navigate up action?
UPDATE
overridden methods onOptionsItemSelected and onSupportNavigateUp doesn't invoked on pressing up button in toolbar
I found a solution
handleOnBackPressed() method invokes only on device's back button click.
I wonder, why neither onOptionsItemSelected() nor onSupportNavigateUp() methods haven't been called on pressing "up button" in toolbar. And the answer is I used
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
in activity to setup toolbar with navigation component.
And that made toolbar responsive for work with navigation internally, pressing "up button" haven't invoked any of overridden methods in activity or fragments.
Instead should be used
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
That will make actionBar responsive for navigation, thus I can use overridden functions onOptionsItemSelected() and onSupportNavigateUp()
And best place (in my case) to add custom behavior on "up button" click for certain screen is
onSupportNavigateUp()
of hosted activity, like that
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.mainNavHostFragment)
return when(navController.currentDestination?.id) {
R.id.destinationOfInterest -> {
// custom behavior here
true
}
else -> navController.navigateUp()
}
}
But worth to say, that if you want implement custom behavior directly in fragment, answer of #Enzokie should work like a charm
You need to call onBackPressed() from onBackPressedDispatcher property. Assuming your Toolbar is properly setup you can use the code below in your Activity.
override fun onOptionsItemSelected(menuItem : MenuItem?) : Boolean {
if (menuItem.getItemId() == android.R.id.home) {
onBackPressedDispatcher.onBackPressed()
return true // must return true to consume it here
}
return super.onOptionsItemSelected(menuItem)
}
on Fragment override
override fun onAttach(context: Context) {
super.onAttach(context)
//enable menu
setHasOptionsMenu(true)
requireActivity()
.onBackPressedDispatcher
.addCallback(this){
//true means that the callback is enabled
this.isEnabled = true
exitDialog() //dialog to conform exit
}
}
What this does is :
Trigger a call to the currently added OnBackPressedCallback
callbacks in reverse order in which they were added. Only if the most
false from its OnBackPressedCallback#handleOnBackPressed()
will any previously added callback be called.
I am using AndroidX in my example therefore my import will look like
import androidx.appcompat.app.AppCompatActivity.
This set up also works and you won't need to override onSupportNavigateUp in your activity:
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
toolbar.setNavigationOnClickListener {
if (navController.currentDestination?.id == R.id.destinationOfInterest) {
// Custom behavior here
} else {
NavigationUI.navigateUp(navController, configuration)
}
}
I prefer to set up the Toolbar since it will handle automatically the up navigation and open/close a DrawerLayout if you have one.
Add click event to toolbar back button in this way
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Toolbar back button pressed, do something you want
default:
return super.onOptionsItemSelected(item);
}
}
Another way
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
// Title and subtitle
toolbar.setNavigationOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// Toolbar back button pressed, do something you want
}
});
I customized (directly in Fragment) the backbress on Toolbar by using the following steps:
1. onCreate [Activity]:
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
2. onSupportNavigateUp [Activity]:
override fun onSupportNavigateUp(): Boolean {
onBackPressedDispatcher.onBackPressed()
return super.onSupportNavigateUp()
}
3. Customize or disable backpress [Fragment]:
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
isEnabled = false
// enable or disable the backpress
}