Provide custom back navigation

Users navigate backward through screens using back navigation. Most Android devices have a back button (physical, software, or gesture-based). Usually, you shouldn't add a back button to your app. However, Android Automotive OS (AAOS) devices in compatibility mode use a system back button. This handles navigation, so you don't need to add your own. For details, see AAOS Compatibility Mode.

Android maintains a back stack of destinations as the user navigates throughout your application. This usually allows Android to properly navigate to previous destinations when the Back button is pressed. However, there are a few cases where your app might need to implement its own Back behavior in order to provide the best possible user experience. For example, when using a WebView, you might want to override the default Back button behavior to allow the user to navigate back through their web browsing history instead of the previous screens in your app.

Implement custom back navigation in Compose

In Jetpack Compose, you can handle custom back navigation using the BackHandler composable.

When using Navigation Compose, you typically use NavController.navigateUp() or NavController.popBackStack() to navigate to the previous screen in the back stack. However, BackHandler is useful for cases where you want to implement custom behavior when the user presses the system back button or uses the back gesture. For example, if you are displaying a WebView in your app, you might want to allow users to navigate back through browsing history when they press the system back button.

Basic usage

Here's an example showing how to intercept the back event to control custom state (like collapsing a panel):

@Composable
fun MyScreen() {
    var isExpanded by remember { mutableStateOf(false) }

    // Intercept the back button or gesture only when isExpanded is true
    BackHandler(enabled = isExpanded) {
        // Collapsing the content disables this handler, so it does not
        // receive the next back event.
        isExpanded = false
    }

    if (isExpanded) {
        Text("Expanded content. Go back to collapse it.")
    } else {
        Button(onClick = { isExpanded = true }) {
            Text("Expand")
        }
    }
}

Composable tree interception

If you have multiple enabled BackHandler composables at different levels of your composable tree, only the innermost one intercepts the back event.

Implement custom back navigation with Views

ComponentActivity, the base class for FragmentActivity and AppCompatActivity, lets you control the behavior of the Back button by using its OnBackPressedDispatcher, which you can retrieve by calling getOnBackPressedDispatcher().

The OnBackPressedDispatcher controls how Back button events are dispatched to one or more OnBackPressedCallback objects. The constructor for OnBackPressedCallback takes a boolean for the initial enabled state. Only when a callback is enabled, for example when isEnabled() returns true, will the dispatcher call the callback's handleOnBackPressed() to handle the Back button event. You can change the enabled state by calling setEnabled().

Callbacks are added using the addCallback methods. Use the addCallback() method which takes a LifecycleOwner. This way the OnBackPressedCallback is only added when the LifecycleOwner is Lifecycle.State.STARTED. The activity also removes registered callbacks when their associated LifecycleOwner is destroyed, which prevents memory leaks and makes it suitable for use in fragments or other lifecycle owners that have a shorter lifetime than the activity.

Here's an example callback implementation:

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
        }

        // The callback can be enabled or disabled here or in the lambda
    }
    ...
}

You can provide multiple callbacks using addCallback(). When doing so, the callbacks are invoked in the reverse order in which they are added—the callback added last is the first given a chance to handle the Back button event. For example, if you added three callbacks named one, two, and three in order, they would be invoked in the order of three, two, and one, respectively.

Callbacks follow the Chain of Responsibility pattern. Each callback in the chain is invoked only if the preceding callback wasn't enabled. This means that in the preceding example, callback two would be invoked only if callback three wasn't enabled. Callback one would only be invoked if callback two wasn't enabled, and this pattern continues for any additional callbacks.

Note that when added using addCallback(), the callback isn't added to the chain of responsibility until the LifecycleOwner enters the Lifecycle.State.STARTED state.

Changing the enabled state on the callback is strongly recommended for temporary changes as it maintains the ordering described earlier, which is particularly important if you have callbacks registered on multiple different nested lifecycle owners.

If you want to remove the callback entirely, call remove(). This usually isn't necessary because callbacks are automatically removed when their associated LifecycleOwner is destroyed.