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
}
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'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()
}
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'm currently running into a strange problem, similar to here Shared element transition when using ActionBar Back button but with a fragment to fragment shared element transition. It works fine when using the back button. As soon as the toolbar home is triggered it's just blinking. So calling finishAfterTransition() is no option here.
I call the same method both times. The base for back navigation
override fun onBackPressed() {
if (coordinator != null) {
coordinator!!.back()
} else {
finish()
}
}
and the intercepted toolbar home click.
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return when (item?.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> return super.onOptionsItemSelected(item)
}
}
Update: Now I noted like one out of ten tries it works correctly.
I hope anyone of you guys has a clue why this happens.
Regards coffeelord
In the activity class, I set up the actionbar as this:
MyActivity
setSupportActionBar(findViewById(R.id.toolbar_my))
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
Because I need to override onOptionsItemSelected(...) (in the fragment class), I didn't override onSupportNavigateUp() here.
This activity cotnains a fragment. What I want is, when click on the actionbar up button, besides pop back, also revoke a custom save() method.
So in the fragment's onOptionsItemSelected(...), write some code for the item.id == android.R.id.home case. However, I made a break point here, and found that when click on the up/home button, the code in the android.R.id.home case is never revoked. The other items' on selected methods work.
In the fragment class:
MyFragment
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> {
// code here not gets called when click up/home button
mPresenter.save()
return true
}
R.id.edit-> {
// The code here is revoked when item selected.
}
else -> {
return super.onOptionsItemSelected(item)
}
}
}
I tried override another onOptionsItemSelected(...) method in the activity class, and write android.R.id.home case, still cannot invoke methods in it.
Why the code in item.id == android.R.id.home case is not called?
Read setHomeButtonEnabled
Enable or disable the "home" button in the corner of the action bar.
(Note that this is the application home/up affordance on the action
bar, not the systemwide home button.)
supportActionBar?.setHomeButtonEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
Then
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.getItemId()){
android.R.id.home -> {
mPresenter.save()
return true
}
R.id.edit-> {
// some code
}
}
return super.onOptionsItemSelected(item)
}