Handling back presses in Jetpack Compose
Tags: android, kotlin, compose, navigation
Published: 2021-04-12 22:35:52

The Navigation component might help us in implementing the navigation between screens in Android apps. There is also a dedicated navigation compose dependency that supports UI declared in Jetpack Compose toolkit.

We just need to create the `NavHost`, pass the `NavController` instance and define composable destinations:

```kotlin
val navController = rememberNavController()

NavHost(
    navController, 
    startDestination = "list"
) {
    composable("list") { List() }
    composable("detail") { Detail() }
}
```

It's very intuitive and straightforward. There is no unnecessary boilerplate code if we want to implement a simple navigation.

I will assume that you already know the basics of Jetpack Compose and the Navigation component. If you don't, I suggest you to catch up first. Jetpack Compose Navigation is well described in the [official docs](https://developer.android.com/jetpack/compose/navigation). 

Time to focus on the thread mentioned in the title - handling back presses. First of all, let's examine the following composable destination declaration:

```kotlin
@Composable
fun Detail() {
    val viewModel = getViewModel<DetailViewModel>()
    val state by viewModel.state.collectAsState()
    Detail(
        state = state,
        onBack = { viewModel.onAction(SelectBack) }
    )
}

@Composable
fun Detail(
    state: DetailState,
    onBack: () -> Unit,
) {
    Scaffold(
        topBar = { DetailTopAppBar(onBack) }
    ) {
        // detail content
    }
}

@Composable
fun DetailTopAppBar(onBack: () -> Unit) {
    TopAppBar(
        title = { Text("Detail") },
        navigationIcon = {
            IconButton(onClick = onBack) {
                Icon(
                    imageVector = Icons.Filled.ArrowBack,
                    contentDescription = "Back",
                )
            }
        }
    )
}
```

I extracted two stateless composables. This makes the code more readable, but primarily it defers the UI declaration from the top composable function that collects the state and interacts with a resolved `ViewModel` instance.

Assume that the logic is extracted to the `ViewModel` class that extends the abstract class below. In this blog post we don't care about its implementation details.

```kotlin
abstract class DetailViewModel : ViewModel() {
    abstract val state: StateFlow<DetailState>
    abstract fun onAction(action: Action)
}
```

It showcases a simple unidirectional data flow, which is a recommended pattern for modelling the UI state and user interaction. This pattern also fits incredibly well with Jetpack Compose.

Let's go back to the point. As you can see above, the `DetailTopAppBar` contains the `IconButton` composable function that invokes the `onBack` lambda. The expression passed in the top `Detail()` function emits a `SelectBack` action and it's consumed by a `DetailViewModel` instance. Then, depending on the business logic, it might navigate up to the parent destination or do something else. It's not the responsibility of the UI layer to think about the consequences of emitting that action. We're skipping the business logic layer.

It's a high time to answer the following question:

*What if a user presses the system back button instead of the navigation icon?*

Given the fact that we use Jetpack Compose Navigation (or we use the `FragmentManager`), we have a back stack and Android will navigate user up to the previous destination. 

But what if we want to intercept the default Back behavior, emitting a `SelectBack` action and thus handle it in the same way as the click on the `TopAppBar` navigation icon?

We could emit an action in the overridden `Activity.onBackPressed()`, but it is ugly and [not recommended](https://developer.android.com/guide/navigation/navigation-custom-back#activity_onbackpressed) solution. It is also not scalable in Single-Activity architectures.

A more elegant way is to create an `OnBackPressedCallback` and add it to the `OnBackPressedDispatcher` that controls dispatching system back presses. We have to enable this callback when we want to handle a back press, which disables other callbacks in the chain of responsibility. Sounds complicated to manage? 

Hopefully, there's a `BackHandler` composable (added in Activity Compose in `1.3.0-alpha02`) that configures it for us. Let's take a brief look at the implementation of that composable function (from `1.3.0-alpha06`):

```kotlin
@Composable
public fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)
    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        object : OnBackPressedCallback(enabled) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }
    // On every successful composition, update the callback with the `enabled` value
    SideEffect {
        backCallback.isEnabled = enabled
    }
    val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
        "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
    }.onBackPressedDispatcher
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(lifecycleOwner, backCallback)
        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}
```

Under the hood, it creates an `OnBackPressedCallback` and enables it when composable function is successfully recomposed. The callback is added on any lifecycle owner change and it's removed on the dispose. Everything is implicitly set and all we need to do is to add the single line of code, which declared the `BackHandler()`:

```kotlin
@Composable
fun Detail(
    state: DetailState,
    onBack: () -> Unit,
) {
    BackHandler(onBack = onBack)
    Scaffold(
        topBar = { DetailTopAppBar(onBack) }
    ) {
        // detail content
    }
}
```

In the same way we can declare the `BackHandler` in different composable destinations, intercepting the default behavior of the system back press. In our case we can emit an action specific to the attached view model. We are also guaranteed that only the inner most enabled `BackHandler` will invoke the `onBack` lambda, which prevents the back press from being handled several times.

> One important note: If you want to use the `BackHandler`, I suggest you to update the Activity Compose to at least `1.3.0-alpha06` and Navigation Compose to at least `1.0.0-alpha10` that depends on Navigation `2.3.5`. In earlier versions of these androidx libraries there was a [bug](https://issuetracker.google.com/issues/182284739), which I reported. It was breaking the back press interception on Activity restart.

If you found this post useful, please tap the :blue_heart:. You can find me on [Twitter](https://twitter.com/pawegio).