In Android 13, the onBackPressed() method was deprecated and replaced by OnBackPressedDispatcher for handling back navigation. This new approach is more flexible and lifecycle-aware, making it ideal for apps with multiple fragments or custom back behavior needs. In this post, we’ll walk through using OnBackPressedDispatcher in activities and fragments to help you manage back navigation effectively.
Let me know your queries in the comments section below.
Cheers!
Happy Coding 🤗
How to use it?
- In your activity/fragment, declare a OnBackPressedCallback variable
- Register the callback in onStart() using onBackPressedDispatcher.addCallback(callback)
- When user performs the back navigation, handleOnBackPressed() will be called in which you can define your custom logic.
- Once you are done listening to back navigation, you can allow the default back navigation by setting isEanble to false.
- Remove the callback in onStop() method using remove() function. This is optional as OnBackPressedCallback is lifecycle aware component, but in some cases it is necessary to avoid unexpected behaviour.
1. Example - Saving form when back is pressed
Consider an example where a user has entered data into a form but presses back before saving it. In this case, we can override the back navigation to display a confirmation prompt, ensuring they don’t lose any unsaved data.- Set enableOnBackInvokedCallback to true in AndroidManifest.xml
<application ... android:enableOnBackInvokedCallback="true"
-
Add few text fiels to your layout file to simulate a form. Here we are creating two fields for name and email.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" tools:context=".ProfileFragment"> <com.google.android.material.textfield.TextInputLayout android:id="@+id/name_field" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/name" app:layout_constraintTop_toTopOf="parent"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout android:id="@+id/email_field" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="@string/email" app:layout_constraintTop_toBottomOf="@id/name_field"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textEmailAddress" /> </com.google.android.material.textfield.TextInputLayout> <Button android:id="@+id/button_save" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/save" app:layout_constraintTop_toBottomOf="@id/email_field" /> </androidx.constraintlayout.widget.ConstraintLayout>
- Open the layout file of your activity/fragment and do the below changes.
- A OnBackPressedCallback is defines and attached to fragment in onStart() and removed in onDestroyView()
- By default we disable the back interceptor by setting isEnabled to false so that the default back navigation can happen.
- A text change lister is added to text fiels and when user inputs the text, the back interceptor is enabled by setting isEnabled to true. When the text fields are blank, we disable the back interceptor.
backPressCallback.isEnabled = !binding.name.text.isNullOrBlank() || !binding.email.text.isNullOrBlank()
- At this point, if user presses the back when form has some data, handleOnBackPressed() is called and a confirmation dialog is shown to save data.
- If user chooses to cancle saving, we navigate back to previous screen by calling findNavController().popBackStack()
package info.androidhive.androidbacknavigation import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import info.androidhive.androidbacknavigation.databinding.FragmentProfileBinding class ProfileFragment : Fragment() { private val binding by lazy { FragmentProfileBinding.inflate(layoutInflater) } private val backPressCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { showConfirmationDialog() } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return binding.root } /** * Showing back press confirmation when form has unsaved data * */ private fun showConfirmationDialog() { context?.let { MaterialAlertDialogBuilder(it).setTitle(resources.getString(R.string.title)) .setMessage(resources.getString(R.string.unsaved_message)) .setNegativeButton(resources.getString(R.string.cancel)) { _, _ -> }.setPositiveButton(resources.getString(R.string.accept)) { _, _ -> findNavController().popBackStack() }.show() } } private var textChangeListener: TextWatcher = object : TextWatcher { override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { // toggle back press callback when form data is changed toggleBackPress() } override fun afterTextChanged(p0: Editable?) {} } /** * Enable back press callback when form has unsaved data * */ private fun toggleBackPress() { backPressCallback.isEnabled = !binding.name.text.isNullOrBlank() || !binding.email.text.isNullOrBlank() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.buttonSave.setOnClickListener { findNavController().popBackStack() } activity?.onBackPressedDispatcher?.addCallback(backPressCallback) // disable back press callback by default backPressCallback.isEnabled = false initForm() } private fun initForm() { binding.apply { name.addTextChangedListener(textChangeListener) email.addTextChangedListener(textChangeListener) } } override fun onDestroyView() { super.onDestroyView() // removing callback is always not necessary // but while using navigation component, the older listener still attached // after back navigation happens backPressCallback.remove() } }
2. Implementing double back press to exit the app
Let's consider another common scenario where the user must press the back button twice to exit the app. This feature is typically implemented to prevent accidental exits. The below example demonstrates how to use the OnBackPressedCallback when having the Navigation Component.- Using navigation component destination listener, the back interceptor is enabled only on home screen. Otherwise, the it will be intercepted in child fragments too.
private val navControllerListener = NavController.OnDestinationChangedListener { _, destination, _ -> // enable back press callback when destination is home fragment backPressCallback.isEnabled = destination.id == R.id.HomeFragment }
- When the back is pressed, a Toast message is shown asking user to press back again immediately. If user presses the back within 1 sec, the app will be exited. Otherwise the back interceptor in enabled again.
private fun showBackToast() { Toast.makeText(this, R.string.press_back_again, Toast.LENGTH_SHORT).show() backPressCallback.isEnabled = false GlobalScope.launch { delay(1000) // user hasn't pressed back within 1 sec. Enable back press callback again backPressCallback.isEnabled = true } }
package info.androidhive.androidbacknavigation
import android.os.Bundle
import android.util.Log
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.navigation.NavController
import info.androidhive.androidbacknavigation.databinding.ActivityMainBinding
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val binding by lazy(LazyThreadSafetyMode.NONE) {
ActivityMainBinding.inflate(layoutInflater)
}
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var navController: NavController
private val backPressCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showBackToast()
}
}
/**
* Listening to navigation destination and enable back press callback only on home screen
* */
private val navControllerListener =
NavController.OnDestinationChangedListener { _, destination, _ ->
// enable back press callback when destination is home fragment
backPressCallback.isEnabled = destination.id == R.id.HomeFragment
}
/**
* Shows toast and disables back press callback. If user presses back again with in 1sec,
* back navigation will happen otherwise back press callback will be enabled again
* */
@OptIn(DelicateCoroutinesApi::class)
private fun showBackToast() {
Toast.makeText(this, R.string.press_back_again, Toast.LENGTH_SHORT).show()
backPressCallback.isEnabled = false
GlobalScope.launch {
delay(1000)
// user hasn't pressed back within 1 sec. Enable back press callback again
backPressCallback.isEnabled = true
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
override fun onStart() {
super.onStart()
onBackPressedDispatcher.addCallback(backPressCallback)
}
override fun onStop() {
super.onStop()
backPressCallback.remove()
}
override fun onResume() {
super.onResume()
navController.addOnDestinationChangedListener(navControllerListener)
}
override fun onPause() {
super.onPause()
navController.removeOnDestinationChangedListener(navControllerListener)
}
}
Cheers!
Happy Coding 🤗
Tags
Tips