Using androix backButtonPressedCallback - android

I need to implement a custom onBackButtonPress method for Fragments. example in LogoutFragment, after logout is handled. user cannot go backStack but a message is shown like press again to exist and is exited. I used this solution. But is not working. Then I saw this Android Doc with onBackPressedDispatcher callback method CODE BELOW. I guess this will work. I added dependencies, but how to implement this in a different fragments, with only 1 activity and fragment container. Kotlin version.
class LogoutFragment : DaggerFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
...
}
...
}
...
}

onBackPressedDispatcher gives an opportunity to handle back press differently in every fragment. Hence you may have to give this piece of code in every fragment.
In the fragment:
var doubleBackToExitPressedOnce = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
val callback = object : OnBackPressedCallback(true ) {
override fun handleOnBackPressed() {
if (doubleBackToExitPressedOnce)
requireActivity().finish()
Toast.makeText(requireContext(), "Press again to go back",
Toast.LENGTH_LONG).show()
doubleBackToExitPressedOnce = true
}
}
If you want to handle back presses of all fragments in one place, you can do so by manipulating the doBack() method in your reference: How to implement onBackPressed() in Fragments?
//pseudocode
fun doBack()
{
//find the fragment
val fragment = supportFragmentManager.findFragmentByTag(
//your fragment tag
)
if(fragment is FragmentA)
{
//do something
}
else if(fragment is FragmentB)
{
//do something else
}
else
{
activity.getSupportFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}

Related

How to add logging to navigation back or up using navigation architecture component?

I am using the navigation component in my Android app, which automatically provides me with back and up navigation. Now I don't want to change any of those behaviours, but I want to add some logging specific to the fragment where the user presses either the up button in the toolbar or the back button.
I tried this, and it worked only for the back button, and I didn't figure out how to leave the default navigation behaviour intact. Plus it seems like this adds a callback at the activity level, so it's not specific to the fragment where I add the callback.
And it seems like onOptionsItemSelected is called for normal menu items, but not for the Up button.
How can I handle this consistently without changing the behaviour of my entire app?
You are very close to the answer.
Try this.
Note: This code is copied from my app. Change as per your requirement.
In Activity:
In onCreate():
// Observe action state live data
activityViewModel.actionStateMutableLiveData.observe(this, Observer { actionState ->
actionState?.let {
if (actionState != "NO_ACTION") {
when (actionState) {
"NAVIGATE_UP" -> {
if (!navController.navigateUp()) {
finish()
}
}
}
// Reset action state
activityViewModel.setActionState("NO_ACTION")
}
}
})
And,
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
navController.currentDestination?.id?.let { currentDestinationId ->
return when (currentDestinationId) {
R.id.fragmentToLog -> {
false
}
else -> {
activityViewModel.setActionState("NAVIGATE_UP")
true
}
}
}
activityViewModel.setActionState("NAVIGATE_UP")
return true
}
else -> {
item.onNavDestinationSelected(findNavController(R.id.fragment_activitymain))
|| super.onOptionsItemSelected(item)
}
}
}
Fragment:
In onViewCreated():
requireActivity().onBackPressedDispatcher.addCallback(this) {
handleNavigateBack()
}
And
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
handleNavigateBack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun handleNavigateBack() {
// TODO: Add your fragment logs here
activityViewModel.setActionState("NAVIGATE_UP")
}
Activity ViewModel:
// Action state
var actionStateMutableLiveData = MutableLiveData(NO_ACTION)
private set
fun setActionState(actionStateValue: String?) =
actionStateMutableLiveData.postValue(actionStateValue)
Concepts used:
MVVM architecture
Live Data & View model
Android architecture navigation components
Please comment if anything is not clear.
in your activity you should set up NavigationUI first by following code
val navController = this.findNavController(R.id.nav_app_id)
NavigationUI.setupActionBarWithNavController(this,navController)
in Fragment you could detect when user click back at toolbar in onOptionsItemSelected and then call findNavController().navigateUp()
One possible solution involves three steps: Define an interface, let the desired fragments implement that interface and override the onSupportNavigateUp / onNavigateUp / onBackPressed method in your hosting activity.
First, define an interface, e.g.:
interface CustomNavAction {
fun logSomeStuff()
}
Secondly, add the interface to the desired Fragment, e.g.:
class AFragment : Fragment(), CustomNavAction {
...
override fun logSomeStuff() {
Log.d("TAG", "Up or back")
}
...
}
Finally, override the onSupportNavigateUp / onNavigateUp / onBackPressed method in the Activity that hosts the navigation component, find the current fragment and check if it implements the interface. If so, you can call the action. For example:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
// find current fragment, and invoke custom action if up button is pressed
val navHostFragment = supportFragmentManager.fragments[0]
val currentFragment = navHostFragment.childFragmentManager.fragments[0]
if (currentFragment is CustomNavAction) {
(currentFragment as CustomNavAction).logSomeStuff()
}
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onBackPressed() {
super.onBackPressed()
...
// similar logic as onSupportNavigateUp
}
In this way, you don't mess with the overall navigation, and only the fragments implementing the interface will log something.
This solution is not specific to the fragment but in this way, you can detect the back press and navigation up with less code
In Fragment, onCreate add this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enable the callback, when we receive a back press event disable the callback and dispatch the event to the activity
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d(
"SomeTag",
"User pressed up back button to navigate back from fragment."
)
isEnabled = false
requireActivity().onBackPressed()
}
})
}
Now, in your activity, you need to override the default ToolBar#onNavigationOnClickListener()
toolbar.setNavigationOnClickListener {
val currentFragment = navHostFragment.childFragmentManager.primaryNavigationFragment
Log.d(
"SomeTag",
"User pressed up arrow to navigate back from fragment: $currentFragment"
)
navController.navigateUp(appBarConfiguration)
}

fragment navigation extension function does not work

I have created this extension function for navigating between fragments which is pretty straight forward but somehow it's not working. it's not doing anything and nothing changes when I click the button. I think the problem is with this#navigate argument but I don't see why that should be a problem.
fun Fragment.navigate(): Int? {
return fragmentManager?.run {
beginTransaction()
.replace(
R.id.my_container,
this#navigate,
this#navigate::class.simpleName
)
.commit()
}
}
and the usage is like this
class TestTwoFragment : Fragment(R.layout.fragment_test_two) {
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
goto_three.setOnClickListener {
TestThreeFragment
.newInstance()
.navigate()
}
}
You're using the fragmentManager of the new fragment which will be null if the fragment hasn't been added yet, like in your case. Since you're using ?.run, nothing happens and the method returns null.
Consider adding a fragment manager parameter to your method:
fun Fragment.navigate(fm: FragmentManager): Int? {
return fm.run {
beginTransaction()
.replace(
R.id.my_container,
this#navigate,
this#navigate::class.simpleName
)
.commit()
}
}
And then:
TestThreeFragment
.newInstance()
.navigate(getParentFragmentManager())

Finishing and removing fragment and going back to the MainActivity

Android Studio 3.4
I have a simple stock forecast app that uses single activity called ForecastActivity and 3 fragments called LoadingFragment, ForecastFragment, and RetryFragment
The RetryFragment will be added first using the following code in the
`ForecastActivity`:
private fun startRetryFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, RetryFragment(), "RetryFragment")
fragmentTransaction.commit()
}
}
I have a button in the RetryFragment that will start the ForecastActivity again.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnRetry.setOnClickListener {
startActivity(Intent(activity, ForecastActivity::class.java))
}
}
Then the LoadingFragment will start using this code in the ForecastActvity
private fun startLoadingFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, LoadingFragment(), "LoadingFragment")
fragmentTransaction.commit()
}
}
Then after loading has completed the ForecatActivity will start the ForecastFragment
private fun startForecastFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, RetryFragment(), "ForecastFragment")
fragmentTransaction.commit()
}
In my ForecastActivity I am trying to remove the fragments from the backstack, basically, from here I just want to finish the app. However, as I didn't add any to the backstack I don't expect the count to greater than zero. However, I was wondering how can I end the app and prevent the RetryFragment displaying again?
override fun onBackPressed() {
fragmentManager?.let {
if(it.backStackEntryCount > 0) {
it.popBackStackImmediate()
}
else {
super.onBackPressed()
}
}
}
However, when I click the back from when I am on the ForecastFragment I expect the application to finish. However, the RetryFragment is displayed and then I have to click the back button again to end the app.
Is there something wrong doing this in the RetryFragment
Basically, I just want to go back to the ForecastActivity from the RetryFragment to start again.
startActivity(Intent(activity, ForecastActivity::class.java))
Many thanks for any suggestions.
I think your issue is that you're re-launching the ForecastActivity again from the RetryFragment. If you restructure your code so that the RetryFragment can trigger a retry function in ForecastActivity, you could just replace the RetryFragment with the next appropriate fragment instead of calling startActivity() to create a new ForecastActivity.

Show confirmation on back/up in Fragment with Navigation Architecture Component

I am using the Navigation Architecture Component for Android.
For one of my fragments I wish to intercept "back" and "up" navigation, so that I can show a confirmation dialog before discarding any unsaved changes by the user. (Same behavior as the default Calendar app when you press back/up after editing event details)
My current approach (untested) is as follows:
For "up" navigation, I override onOptionsItemSelected on the fragment:
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if(item?.itemId == android.R.id.home) {
if(unsavedChangesExist()) {
// TODO: show confirmation dialog
return true
}
}
return super.onOptionsItemSelected(item)
}
For "back" navigation, I created a custom interface and callback system between the fragment and its activity:
interface BackHandler {
fun onBackPressed(): Boolean
}
class MainActivity : AppCompatActivity() {
...
val backHandlers: MutableSet<BackHandler> = mutableSetOf()
override fun onBackPressed() {
for(handler in backHandlers) {
if(handler.onBackPressed()) {
return
}
}
super.onBackPressed()
}
...
}
class MyFragment: Fragment(), BackHandler {
...
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is MainActivity) {
context.backHandlers.add(this)
}
}
override fun onDetach() {
(activity as? MainActivity)?.backHandlers?.remove(this)
super.onDetach()
}
override fun onBackPressed(): Boolean {
if(unsavedChangedExist()) {
// TODO: show confirmation dialog
return true
}
}
...
}
This is all pretty gross and boilerplatey for such a simple thing. Is there a better way?
As of androidx.appcompat:appcompat:1.1.0-beta01, in order to intercept the back button with navigation component you need to add a callback to the OnBackPressedDispatcher. This callback has to extend OnBackPressedCallback and override handleOnBackPressed. OnBackPressedDispatcher follows a chain of responsibility pattern to handle the callbacks. In other words, if you set your callback as enabled, only your callback will be executed. Otherwise, OnBackPressedDispatcher will ignore it and proceed to the next callback, and so on until it finds an enabled one (this might be useful when you have more than one callback, for instance). More info on this here.
So, in order to show your dialog, you would have to do something similar to this in your Fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Show your dialog and handle navigation
}
// you can enable/disable the callback here by setting
// callback.isEnabled = true/false. Or just enable it in the lambda.
}
That addCallback method takes in a LifecycleOwner, and it will make sure that the callback is added when LifecycleOwner reaches the STARTED stage. Not only that, but this also makes it so that the callback is removed when its associated LifecycleOwner is destroyed.
As for the up button, it seems like (at least for now) there aren't many possibilities. The only option I could find up until now that uses the navigation component is to add a listener for the navigation itself, which would handle both buttons at the same time:
navController.addOnDestinationChangedListener { navController, destination ->
if (destination.id == R.id.destination) {
// do your thing
}
}
Regardless, this has the caveat of allowing the activity or fragment where you add the listener to know about destinations it might not be supposed to.
With the navigation architecture components, you can do something like this:
Tell your activity to dispatch all up clicks on the home button(back arrow) to anyone listening for it. This goes in your activity.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressedDispatcher.onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
Then in your fragments, consume the events like so
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this) {
if (*condition for showing dialog here*) {
// Show dialog
} else {
// pop fragment by calling function below. Analogous to when the user presses the system UP button when the associated navigation host has focus.
findNavController().navigateUp()
}
}
}
for up navigation simply override onOptionsItemSelected()
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> {
showDialog() // show your dialog here
true
}
else -> super.onOptionsItemSelected(item)
}
You can used following function in onAttach in your fragment to override the onBackPressed() with help of the navigation components.
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (YOUR_CONDITION) {
// Do something here
} else {
if (!findNavController().navigateUp()) {
if (isEnabled) {
isEnabled = false
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
}
}
}
)
If you're using it with AppBarConfiguration, with the latest release there is now an AppBarConfiguration.OnNavigateUpListener. Refer to the below link for more information
https://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener
if u override onBackPressed() in your activity must ensure that it should call super.onBackOnBackPressed() otherwise these dispatcher wont trigger

Handling back button in Android Navigation Component

I'd like to know how properly handle system back button action using Navigation Controller. In my app I have two fragments (for ex. fragment1 and fragment2) and I have an action in fragment1 with destination to fragment2. Everything works well except one thing - when user presses system back button in fragment2 I want to show a dialog (using DialogFragment for example) to confirm exit. What is the best way to implement this behavior? If I use app:defaultNavHost="true" in my host fragment then it automatically goes back ignoring my rules. And, additionally, what is this component for?
Should I use "pop to" may be?
Newest Update - April 25th, 2019
New release androidx.activity ver. 1.0.0-alpha07 brings some changes
More explanations in android official guide: Provide custom back navigation
Example:
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()
}
...
}
Old Updates
UPD: April 3rd, 2019
Now its simplified. More info here
Example:
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), this);
#Override
public boolean handleOnBackPressed() {
//Do your job here
//use next line if you just need navigate up
//NavHostFragment.findNavController(this).navigateUp();
//Log.e(getClass().getSimpleName(), "handleOnBackPressed");
return true;
}
Deprecated (since Version 1.0.0-alpha06
April 3rd, 2019) :
Since this, it can be implemented just using JetPack implementation OnBackPressedCallback in your fragment
and add it to activity:
getActivity().addOnBackPressedCallback(getViewLifecycleOwner(),this);
Your fragment should looks like this:
public MyFragment extends Fragment implements OnBackPressedCallback {
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getActivity().addOnBackPressedCallback(getViewLifecycleOwner(),this);
}
#Override
public boolean handleOnBackPressed() {
//Do your job here
//use next line if you just need navigate up
//NavHostFragment.findNavController(this).navigateUp();
//Log.e(getClass().getSimpleName(), "handleOnBackPressed");
return true;
}
#Override
public void onDestroyView() {
super.onDestroyView();
getActivity().removeOnBackPressedCallback(this);
}
}
UPD:
Your activity should extends AppCompatActivityor FragmentActivity and in Gradle file:
implementation 'androidx.appcompat:appcompat:{lastVersion}'
For anyone looking for a Kotlin implementation see below.
Note that the OnBackPressedCallback only seems to work for providing custom back behavior to the built-in software/hardware back button and not the back arrow button/home as up button within the actionbar/toolbar. To also override the behavior for the actionbar/toolbar back button I'm providing the solution that's working for me. If this is a bug or you are aware of a better solution for that case please comment.
build.gradle
...
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
implementation "androidx.navigation:navigation-fragment-ktx:2.0.0"
implementation "androidx.navigation:navigation-ui-ktx:2.0.0"
...
MainActivity.kt
...
import androidx.appcompat.app.AppCompatActivity
...
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
...
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(navController.graph)
// This line is only necessary if using the default action bar.
setupActionBarWithNavController(navController, appBarConfiguration)
// This remaining block is only necessary if using a Toolbar from your layout.
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setupWithNavController(navController, appBarConfiguration)
// This will handle back actions initiated by the the back arrow
// at the start of the toolbar.
toolbar.setNavigationOnClickListener {
// Handle the back button event and return to override
// the default behavior the same way as the OnBackPressedCallback.
// TODO(reason: handle custom back behavior here if desired.)
// If no custom behavior was handled perform the default action.
navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
/**
* If using the default action bar this must be overridden.
* This will handle back actions initiated by the the back arrow
* at the start of the action bar.
*/
override fun onSupportNavigateUp(): Boolean {
// Handle the back button event and return true to override
// the default behavior the same way as the OnBackPressedCallback.
// TODO(reason: handle custom back behavior here if desired.)
// If no custom behavior was handled perform the default action.
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
MyFragment.kt
...
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
...
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Handle the back button event
}
}
requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback)
}
}
The official documentation can be viewed at https://developer.android.com/guide/navigation/navigation-custom-back
So, I created an interface
public interface OnBackPressedListener {
void onBackPressed();
}
And implemented it by all fragments that need to handle back button. In main activity I overrided onBackPressed() method:
#Override
public void onBackPressed() {
final Fragment currentFragment = mNavHostFragment.getChildFragmentManager().getFragments().get(0);
final NavController controller = Navigation.findNavController(this, R.id.nav_host_fragment);
if (currentFragment instanceof OnBackPressedListener)
((OnBackPressedListener) currentFragment).onBackPressed();
else if (!controller.popBackStack())
finish();
}
So, If the top fragment of my Navigation host implements OnBackPressedListener interface, I call its onBackPressed() method, elsewhere I simply pop back stack and close application if the back stack is empty.
The recommended approach is to add an OnBackPressedCallback to the activity's OnBackPressedDispatcher.
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
// handle back event
}
Here is solution that should do what you want, but i think it is a bad solution, because it is going against Android Navigation component idea(letting the android handle the navigation).
Override "onBackPressed" inside your activity
override fun onBackPressed() {
when(NavHostFragment.findNavController(nav_host_fragment).currentDestination.id) {
R.id.fragment2-> {
val dialog=AlertDialog.Builder(this).setMessage("Hello").setPositiveButton("Ok", DialogInterface.OnClickListener { dialogInterface, i ->
finish()
}).show()
}
else -> {
super.onBackPressed()
}
}
}
I written in main activity like this,
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.my_nav_host_fragment).navigateUp(appBarConfiguration)
}
Just add these lines
override fun onBackPressed() {
if(navController.popBackStack().not()) {
//Last fragment: Do your operation here
finish()
}
navController.popBackStack() will just pop your fragment if this is not your last fragment
In 2.1.0-alpha06
If you want to handle backpress only in current fragment
requireActivity().onBackPressedDispatcher.addCallback(this#LoginFragment) {
// handle back event
}
For whole Activity
requireActivity().onBackPressedDispatcher.addCallback() {
// handle back event
}
Update
Apr 22, '21
I'm updating my answer to showcase a sample of the recommended approach which is also the accepted answer above.
class MyFragment : Fragment() {
...
private val backPressedDispatcher = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Redirect to our own function
this#MyFragment.onBackPressed()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
setHasOptionsMenu(true) //Set this to true in order to trigger callbacks to Fragment#onOptionsItemSelected
(requireActivity() as AppCompatActivity).apply {
// Redirect system "Back" press to our dispatcher
onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedDispatcher)
// Set toolbar if it is in Fragment's layout. If you have a global toolbar that lives in Activity layout, then you don't need this line.
setSupportActionBar(view.findViewById(R.id.toolbar))
// Setup action bar to work with NavController
setupActionBarWithNavController(findNavController())
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) {
// Redirect "Up/Home" button clicks to our own function
this#MyFragment.onBackPressed()
true
} else {
super.onOptionsItemSelected(item)
}
}
private fun onBackPressed() {
// Work your magic! Show dialog etc.
}
override fun onDestroyView() {
// It is optional to remove since our dispatcher is lifecycle-aware. But it wouldn't hurt to just remove it to be on the safe side.
backPressedDispatcher.remove()
super.onDestroyView()
}
}
Original answer
Jan 3 '19
A little late to the party, but with the latest release of Navigation Component 1.0.0-alpha09, now we have an AppBarConfiguration.OnNavigateUpListener.
Refer to these links for more information:
https://developer.android.com/reference/androidx/navigation/ui/AppBarConfiguration.OnNavigateUpListener
https://developer.android.com/jetpack/docs/release-notes
FragmentExtenstions.kt
fun Fragment.onBackPressedCustomAction(action: () -> Unit) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override
fun handleOnBackPressed() {
action()
}
})
}
YourPrettyFragment.kt
onBackPressedCustomAction {
// Your custom action here
}
This is 2 lines of code can listen for back press, from fragments, [TESTED and WORKING]
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
//setEnabled(false); // call this to disable listener
//remove(); // call to remove listener
//Toast.makeText(getContext(), "Listing for back press from this fragment", Toast.LENGTH_SHORT).show();
}
The recommended method worked for me but after updating my library implementation 'androidx.appcompat:appcompat:1.1.0'
Implement as below
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Handle the back button event
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
using Kotlin
you can provide your custom back navigation by using OnBackPressedDispatcher
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This callback will only be called when MyFragment is at least Started.
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Handle the back button event
// and if you want to need navigate up
//NavHostFragment.findNavController(this).navigateUp()
}
// The callback can be enabled or disabled here or in the lambda
}
}
More explanations in android official guide: https://developer.android.com/guide/navigation/navigation-custom-back
Use this if you're using fragment or add it in your button click listener. This works for me.
requireActivity().onBackPressed()
Called when the activity has detected the user's press of the back key. The getOnBackPressedDispatcher() OnBackPressedDispatcher} will be given chance to handle the back button before the default behavior of android.app.Activity#onBackPressed()} is invoked.
Using navigation components
This was good for me:
Navigation.findNavController(requireView()).popBackStack()
android documentation
If you use Navigation Component follow the codes below in your onCreateView() method (in this example I want just to close my app by this fragment)
OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
new AlertDialog.Builder(Objects.requireNonNull(getActivity()))
.setIcon(R.drawable.icon_01)
.setTitle(getResources().getString(R.string.close_app_title))
.setMessage(getResources().getString(R.string.close_app_message))
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
getActivity().finish();
}
})
.setNegativeButton(R.string.no, null)
.show();
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, backPressedCallback);
And if you want the same behavior also for the toolbar back button just add this in your activity:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
just create an extension function to the fragment
fun Fragment.onBackPressedAction(action: () -> Boolean) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
this.isEnabled = action()
if (!this.isEnabled) {
requireActivity().onBackPressed()
}
}
})
}
and after in the fragment put the code into onCreateView (the action must return false to call the activity onBackPressed)
onBackPressedAction { //do something }
Kotlin Answer
Use popBackStack() example:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mButton.setOnClickListener {
Navigation.findNavController(view).popBackStack() // You need this line.
}
}
If you are using BaseFragment for your app then you can add onBackPressedDispatcher to your base fragment.
//Make a BaseFragment for all your fragments
abstract class BaseFragment : Fragment() {
private lateinit var callback: OnBackPressedCallback
/**
* SetBackButtonDispatcher in OnCreate
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setBackButtonDispatcher()
}
/**
* Adding BackButtonDispatcher callback to activity
*/
private fun setBackButtonDispatcher() {
callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
/**
* Override this method into your fragment to handleBackButton
*/
open fun onBackPressed() {
}
}
Override onBackPressed() in your fragment by extending basefragment
//How to use this into your fragment
class MyFragment() : BaseFragment(){
private lateinit var mView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mView = inflater.inflate(R.layout.fragment_my, container, false)
return mView.rootView
}
override fun onBackPressed() {
//Write your code here on back pressed.
}
}
Try this. I think this will help you.
override fun onBackPressed() {
when (mNavController.getCurrentDestination()!!.getId()) {
R.id.loginFragment -> {
onWarningAlertDialog(this, "Alert", "Do you want to close this application ?")
}
R.id.registerFragment -> {
super.onBackPressed()
}
}
}
private fun onWarningAlertDialog(mainActivity: MainActivity, s: String, s1: String) {
val dialogBuilder = AlertDialog.Builder(this)
dialogBuilder.setMessage(/*""*/s1)
.setCancelable(false)
.setPositiveButton("Proceed", DialogInterface.OnClickListener { dialog, id ->
finish()
})
.setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, id ->
dialog.cancel()
})
// create dialog box
val alert = dialogBuilder.create()
// set title for alert dialog box
alert.setTitle("AlertDialogExample")
// show alert dialog
alert.show()
}
if you are actually trying to handle back button specifically then you could use #Jurij Pitulja answer.
But, if you want pop SecondFragment (start fragment FirstFragment) and not return to FirstFragment, then you could use :
Navigation.findNavController(view).popBackStack()
from the SecondFragment. This way you would pop the SecondFragmetn of the back stack, and not return to SecondFragment when you press back button from FirstFragment.
This is an answer if you are using the NavController:
Navigation.findNavController(view).navigateUp();
Don't try to hack other methods, like replacing the fragment using FragmentManager and replace() in a transaction. The NavController takes care of this.
Here is my solution
Use androidx.appcompat.app.AppCompatActivity for the activity that contains the NavHostFragment fragment.
Define the following interface and implement it in all navigation destination fragments
interface InterceptionInterface {
fun onNavigationUp(): Boolean
fun onBackPressed(): Boolean
}
In your activity override onSupportNavigateUp and onBackPressed:
override fun onSupportNavigateUp(): Boolean {
return getCurrentNavDest().onNavigationUp() || navigation_host_fragment.findNavController().navigateUp()
}
override fun onBackPressed() {
if (!getCurrentNavDest().onBackPressed()){
super.onBackPressed()
}
}
private fun getCurrentNavDest(): InterceptionInterface {
val currentFragment = navigation_host_fragment.childFragmentManager.primaryNavigationFragment as InterceptionInterface
return currentFragment
}
This solution has the advantage, that the navigation destination fragments don't need to worry about the unregistering of their listeners as soon as they are detached.
I tried Jurij Pitulja solution but I just wasn't able to find getOnBackPressedDispatcher or addOnBackPressedCallback
also using Kiryl Tkach's solution wasn't able to find the current fragment, so here's mine:
interface OnBackPressedListener {
fun onBackPressed(): Boolean
}
override fun onBackPressed() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
val currentFragment = navHostFragment?.childFragmentManager!!.fragments[0]
if (currentFragment !is OnBackPressedListener || !(currentFragment as OnBackPressedListener).onBackPressed()) super.onBackPressed()
this way you can decide in fragment whether the activity should take control of back pressed or not.
Alternatively, you have BaseActivity for all your activities, you can implement like this
override fun onBackPressed() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
if (navHostFragment != null){
val currentFragment = navHostFragment.childFragmentManager.fragments[0]
if (currentFragment !is AuthContract.OnBackPressedListener ||
!(currentFragment as AuthContract.OnBackPressedListener).onBackPressed()) super.onBackPressed()
} else {
super.onBackPressed()
}
}
Depending on your logic, if you want to close only the current fragment you have to pass viewLifecycleOwner, code is shown below:
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
requireActivity().finish()
}
})
However, if you want to close application on backPressed no matter from what fragment(probably you wouldn't want that!), don't pass the viewLifecycleOwner. Also if you want to disable the back button, do not do anything inside the handleOnBackPressed(), see below:
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// do nothing it will disable the back button
}
})
My Opinion
requireActivity().onBackPressed()
requireActivity().onBackPressed()
I have searched through many threads and none of them work. Finally I found one:
MainActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar mToolbar = findViewById(R.id.topAppBar);
setSupportActionBar(mToolbar);
}
#Override
public boolean onSupportNavigateUp() {
navController.navigateUp();
return super.onSupportNavigateUp();
}
MyFragment.java
#Override
public void onViewCreated(#NonNull final View view, #Nullable Bundle savedInstanceState) {
Toolbar mToolbar = (MainActivity) getActivity().findViewById(R.id.topAppBar);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Do something when uses presses back button (showing modals, messages,...)
// Note that this will override behaviour of back button
}
});
}
#Override
public void onStop() {
// Reset back button to default behaviour when we leave this fragment
Toolbar mToolbar = (MainActivity) getActivity().findViewById(R.id.topAppBar);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mainActivity.onBackPressed();
}
});
super.onStop();
}
I need to support both real back button and toolbar back button with ability to override "Back" click in both cases (to show dialog or something else). I made an additional method in activity and corresponding boolean checks ('onBackPressed' in my case) in fragments:
// Process hardware Back button
override fun onBackPressed() {
if (canCloseActivity()) {
super.onBackPressed()
}
}
// Process toobar Back and Menu button
override fun onSupportNavigateUp(): Boolean {
if (canCloseActivity()) {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
return false
}
// Do real check if has unfinished tasks, return false to override activity closing
private fun canCloseActivity(): Boolean {
val currentFragment = navHostFragment.childFragmentManager.primaryNavigationFragment
return when {
currentFragment is MyFragment && currentFragment.onBackPressed() -> false
drawerLayout.isOpen -> {
drawerLayout.close()
false
}
fullScreenPreviewLayout.visibility == View.VISIBLE -> {
closeFullscreenPreview()
false
}
else -> true
}
}
Simply, in onCreate() method of your Fragment use this code after super.onCreate(savedInstanceState):
// This callback will only be called when MyFragment is at least Started.
val callback = requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
// Handle the back button event
}

Categories

Resources