How Back Button Handling Works Differently on Android vs iOS in Muse
Muse implements back button handling via a Kotlin Multiplatform expect function that intercepts hardware back presses on Android using OnBackPressedDispatcher, while the iOS implementation is intentionally a no-op because the platform lacks a system back button.
The Muse repository demonstrates how back button handling is implemented differently on Android versus iOS within a shared Compose Multiplatform codebase. The solution uses Kotlin's expect/actual mechanism to provide platform-specific behavior while exposing a uniform API to common UI code.
Common API Declaration
The shared interface resides in muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/BackHandler.kt. It declares an expect composable function that accepts a lambda to execute when a back action occurs.
package io.github.kkoshin.muse.platformbridge
import androidx.compose.runtime.Composable
@Composable
expect fun BackHandler(onBack: () -> Unit)
This declaration allows any composable in the common source set to register back-navigation logic without knowing platform specifics.
Android Implementation
In muse/src/androidMain/kotlin/io/github/kkoshin/muse/platformbridge/BackHandler.android.kt, the actual implementation hooks into the Android activity's back-press dispatcher. It uses LocalOnBackPressedDispatcherOwner to obtain the current dispatcher and registers an OnBackPressedCallback via a DisposableEffect.
package io.github.kkoshin.muse.platformbridge
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.activity.OnBackPressedCallback
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@Composable
actual fun BackHandler(onBack: () -> Unit) {
val dispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
DisposableEffect(dispatcher) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBack()
}
}
dispatcher?.addCallback(callback)
onDispose {
callback.remove()
}
}
}
The enabled flag set to true ensures the callback is active immediately. When the user presses the hardware back button, Android invokes handleOnBackPressed(), executing the shared lambda provided by the caller. The DisposableEffect guarantees cleanup when the composable leaves the composition, preventing memory leaks.
iOS Implementation
Conversely, muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/BackHandler.ios.kt provides a no-op implementation. Because iOS devices do not expose a system back button (navigation relies on swipe gestures or custom UI elements), the actual function body is empty.
package io.github.kkoshin.muse.platformbridge
import androidx.compose.runtime.Composable
@Composable
actual fun BackHandler(onBack: () -> Unit) {
// iOS does not have a system back button in the same way as Android.
// No-op implementation.
}
This design allows shared code to call BackHandler unconditionally on both platforms without compilation errors or runtime crashes on iOS.
Using BackHandler in Shared Code
Developers consume the API identically across platforms. The following snippet demonstrates a settings screen that intercepts back navigation on Android while remaining harmless on iOS.
@Composable
fun SettingsScreen(onClose: () -> Unit) {
BackHandler {
// Custom back logic executes only on Android
onClose()
}
// UI content...
}
On Android, pressing the hardware back button triggers onClose(). On iOS, the composable renders without side effects, leaving navigation to the SwiftUI hosting container or custom gesture handlers.
Summary
- Common contract:
BackHandler.ktdeclares anexpect funproviding a platform-agnostic API. - Android behavior:
BackHandler.android.ktregisters anOnBackPressedCallbackwith the activity dispatcher to intercept hardware back presses. - iOS behavior:
BackHandler.ios.ktimplements an empty function because iOS lacks a system back button equivalent. - Integration: UI code in
commonMaincallsBackHandleronce; the Kotlin Multiplatform toolchain resolves the correct implementation at compile time.
Frequently Asked Questions
Why is the iOS implementation a no-op?
iOS devices do not provide a hardware back button; navigation occurs via swipe gestures or custom toolbar buttons managed by SwiftUI or UIKit. Therefore, the iOS actual implementation in BackHandler.ios.kt intentionally performs no action, avoiding conflicts with the platform's native navigation stack.
How does Android intercept the hardware back button?
The Android implementation leverages LocalOnBackPressedDispatcherOwner to access the current activity's OnBackPressedDispatcher. It creates an enabled OnBackPressedCallback inside a DisposableEffect (located in BackHandler.android.kt), which Android invokes when the user presses the hardware back key.
Can I use BackHandler on iOS for custom swipe-back gestures?
No. The current implementation in BackHandler.ios.kt is a hard-coded no-op. To handle iOS-specific gestures, you must implement separate expect/actual pairs or use platform-specific SwiftUI modifiers outside the shared BackHandler API.
Where is the common BackHandler API defined?
The shared expect declaration resides in muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/BackHandler.kt. This file serves as the single source of truth for the API signature used across both Android and iOS source sets.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →