From 419fbac01c4a7961fc6a4125b2ba18673bc3abd4 Mon Sep 17 00:00:00 2001 From: George Polak Date: Tue, 26 May 2026 14:53:12 -0400 Subject: [PATCH 1/6] handle predictive back --- CHANGELOG.md | 4 + PREDICTIVE_BACK.md | 78 +++++++++++++++++ gradle/libs.versions.toml | 2 + libraries/rib-android/build.gradle.kts | 1 + .../uber/rib/core/PredictiveBackHandler.kt | 43 ++++++++++ .../kotlin/com/uber/rib/core/RibActivity.kt | 52 ++++++++---- .../com/uber/rib/core/RibActivityTest.kt | 84 +++++++++++++++++++ .../com/uber/rib/core/StackRouterNavigator.kt | 12 +++ 8 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 PREDICTIVE_BACK.md create mode 100644 libraries/rib-android/src/main/kotlin/com/uber/rib/core/PredictiveBackHandler.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index c34b822f2..aade40eef 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Unreleased + +* [Android] Support Android Predictive Back gesture. `RibActivity` now registers an `OnBackPressedCallback` instead of overriding the deprecated `onBackPressed()`. No changes required to existing interactors or routers. See [PREDICTIVE_BACK.md](PREDICTIVE_BACK.md) for opt-in instructions and migration details. + ### Version 0.1.0 * Initial release diff --git a/PREDICTIVE_BACK.md b/PREDICTIVE_BACK.md new file mode 100644 index 000000000..25c8f48fe --- /dev/null +++ b/PREDICTIVE_BACK.md @@ -0,0 +1,78 @@ +# Predictive Back Gesture Support + +Android 14 (API 34) introduced the [Predictive Back gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture), which shows a preview animation of where a back swipe will land before the user commits to it. Apps must explicitly opt in and use the modern back-navigation APIs to get these animations. + +## What changed in RIBs + +`RibActivity` previously intercepted back presses by overriding `onBackPressed()`, which is deprecated for back-interception purposes on Android 13+. It now registers an `OnBackPressedCallback` with `onBackPressedDispatcher` instead. + +The internal RIBs back-handling chain is **unchanged**: + +``` +OnBackPressedCallback (in RibActivity) + └─ Router.handleBackPress() + └─ Interactor.handleBackPress(): Boolean +``` + +Interactors that override `handleBackPress()` and `ScreenStackBase` / `StackRouterNavigator` users require **no changes**. + +## Opting in to Predictive Back animations + +To enable the system back animations (swipe-to-home, cross-activity, cross-task), add the following to your app's `AndroidManifest.xml`: + +```xml + +``` + +This flag is what triggers the visual preview animations on Android 14+. Without it, back navigation continues to work exactly as before — the `RibActivity` change is fully backward-compatible regardless of this flag. + +You can also opt individual activities in or out: + +```xml + +``` + +## Custom in-app back animations + +If you want to drive your own animated preview (e.g., a custom route transition) during the back swipe, override `handleOnStarted`, `handleOnProgressed`, `handleOnCancelled`, and `handleOnBackPressed` in a custom `OnBackPressedCallback` and register it **before** the RIBs callback in your activity's `onCreate`: + +```kotlin +class MyActivity : RibActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + // Register custom callback first so it sits ahead of RibActivity's in the chain. + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // Only intercept when your custom animation applies; otherwise disable + // this callback so RibActivity's callback takes over. + if (shouldAnimateCustomTransition()) { + runCustomBackAnimation() + } else { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + }) + super.onCreate(savedInstanceState) + } +} +``` + +## SDK requirements + +| Requirement | Version | +|---|---| +| `androidx.activity` (transitive via `androidx.appcompat`) | 1.6.0+ | +| Predictive Back animations visible to users | Android 14+ (API 34) | +| `android:enableOnBackInvokedCallback` manifest flag | Android 13+ (API 33) — ignored on older OS versions | + +No changes to your `build.gradle` files are needed. `androidx.appcompat:1.6.x` already provides the required `OnBackPressedCallback` API. + +## Legacy behavior + +Apps that do **not** add `android:enableOnBackInvokedCallback="true"` to their manifest are unaffected. Back navigation continues to work identically to before on all API levels. The change to `RibActivity` is purely internal. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0aa42a159..0dd406063 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] android-api = "4.1.1.4" android-studio = "2023.3.1.20" +androidx-activity = "1.9.0" androidx-annotation = "1.9.1" androidx-appcompat = "1.6.1" androidx-lifecycle = "2.6.2" @@ -37,6 +38,7 @@ savedstate = "1.2.1" [libraries] android-api = { group = "com.google.android", name = "android", version.ref = "android-api" } +androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidx-activity" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidx-annotation" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } autocommon = { group = "com.google.auto", name = "auto-common", version.ref = "autocommon" } diff --git a/libraries/rib-android/build.gradle.kts b/libraries/rib-android/build.gradle.kts index c92a4913d..40def2bf7 100644 --- a/libraries/rib-android/build.gradle.kts +++ b/libraries/rib-android/build.gradle.kts @@ -29,6 +29,7 @@ kotlin.compilerOptions { dependencies { api(project(":libraries:rib-android-core")) api(project(":libraries:rib-base")) + api(libs.androidx.activity) api(libs.rxkotlin) api(libs.rxrelay2) api(libs.rxjava2) diff --git a/libraries/rib-android/src/main/kotlin/com/uber/rib/core/PredictiveBackHandler.kt b/libraries/rib-android/src/main/kotlin/com/uber/rib/core/PredictiveBackHandler.kt new file mode 100644 index 000000000..266842d7c --- /dev/null +++ b/libraries/rib-android/src/main/kotlin/com/uber/rib/core/PredictiveBackHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.uber.rib.core + +import androidx.activity.BackEventCompat + +/** + * Optional interface for [ViewRouter]s that want to drive a custom predictive back animation. + * + * [RibActivity] checks whether the root router implements this interface and, when it does, + * forwards the three gesture-progress callbacks so the router can animate the transition as the + * user swipes. [handleBackPress] is still the commit point and remains on [Router]. + * + * All methods have empty defaults so implementors only override what they need. + * + * These callbacks are only invoked by the system on devices that support predictive back (Android + * 14+ with the gesture enabled). On older devices or when the gesture is not active they are never + * called, so implementing this interface has no effect on legacy back behaviour. + */ +public interface PredictiveBackHandler { + + /** Called when the predictive back gesture is first detected. */ + public fun onBackStarted(backEvent: BackEventCompat) {} + + /** Called continuously as the user's finger moves during the back swipe. */ + public fun onBackProgressed(backEvent: BackEventCompat) {} + + /** Called when the user cancels the gesture (lifts finger without committing). */ + public fun onBackCancelled() {} +} diff --git a/libraries/rib-android/src/main/kotlin/com/uber/rib/core/RibActivity.kt b/libraries/rib-android/src/main/kotlin/com/uber/rib/core/RibActivity.kt index a0b76d6a9..28e6f2bd1 100644 --- a/libraries/rib-android/src/main/kotlin/com/uber/rib/core/RibActivity.kt +++ b/libraries/rib-android/src/main/kotlin/com/uber/rib/core/RibActivity.kt @@ -19,6 +19,7 @@ import android.content.Intent import android.content.res.Configuration import android.os.Build import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback import androidx.annotation.CallSuper import com.uber.autodispose.lifecycle.CorrespondingEventsFunction import com.uber.autodispose.lifecycle.LifecycleEndedException @@ -50,6 +51,39 @@ public abstract class RibActivity : RxActivityEvents { private var router: ViewRouter<*, *>? = null + private val ribBackPressCallback = + object : OnBackPressedCallback(true) { + override fun handleOnBackStarted(backEvent: androidx.activity.BackEventCompat) { + (router as? PredictiveBackHandler)?.onBackStarted(backEvent) + } + + override fun handleOnBackProgressed(backEvent: androidx.activity.BackEventCompat) { + (router as? PredictiveBackHandler)?.onBackProgressed(backEvent) + } + + override fun handleOnBackCancelled() { + (router as? PredictiveBackHandler)?.onBackCancelled() + } + + override fun handleOnBackPressed() { + if (router?.handleBackPress() != true) { + onUnhandledBackPressed() + // https://issuetracker.google.com/issues/139738913 + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + isTaskRoot && + supportFragmentManager.backStackEntryCount == 0 + ) { + finishAfterTransition() + } else { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + } + } + private val _lifecycleFlow = MutableSharedFlow(1, 0, BufferOverflow.DROP_OLDEST) @@ -88,6 +122,7 @@ public abstract class RibActivity : @CallSuper override fun onCreate(savedInstanceState: android.os.Bundle?) { super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this, ribBackPressCallback) val rootViewGroup = findViewById(android.R.id.content) _lifecycleFlow.tryEmit(createOnCreateEvent(savedInstanceState)) val wrappedBundle: Bundle? = @@ -184,23 +219,6 @@ public abstract class RibActivity : ) } - override fun onBackPressed() { - if (router?.handleBackPress() != true) { - onUnhandledBackPressed() - - // https://issuetracker.google.com/issues/139738913 - if ( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - isTaskRoot && - supportFragmentManager.backStackEntryCount == 0 - ) { - super.finishAfterTransition() - } else { - super.onBackPressed() - } - } - } - override fun onUserLeaveHint() { _lifecycleFlow.tryEmit(create(ActivityLifecycleEvent.Type.USER_LEAVING)) super.onUserLeaveHint() diff --git a/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt b/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt index a0941ca54..f7cfb2e1e 100644 --- a/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt +++ b/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt @@ -321,6 +321,46 @@ class RibActivityTest { } } + /** Activity whose back-press handling and [onUnhandledBackPressed] calls are observable. */ + private class BackPressActivity : RibActivity() { + var unhandledBackPressCount = 0 + private set + + private lateinit var backAwareInteractor: BackAwareInteractor + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + setTheme(R.style.Theme_AppCompat) + super.onCreate(savedInstanceState) + } + + override fun createRouter(parentViewGroup: ViewGroup): ViewRouter<*, *> { + val view = FrameLayout(this) + val presenter = object : ViewPresenter(view) {} + val component: InteractorComponent, *> = mock { + on { presenter() } doReturn presenter + } + backAwareInteractor = BackAwareInteractor(presenter) + return object : + ViewRouter(view, backAwareInteractor, component) {} + } + + override fun onUnhandledBackPressed() { + unhandledBackPressCount++ + } + + fun setHandlesBackPress(handles: Boolean) { + backAwareInteractor.handlesBackPress = handles + } + } + + private class BackAwareInteractor( + presenter: ViewPresenter<*>, + ) : Interactor, FakeRouter<*>>(presenter) { + var handlesBackPress = false + + override fun handleBackPress(): Boolean = handlesBackPress + } + private class EmptyRouter( view: FrameLayout, interactor: Interactor, *>, @@ -343,6 +383,50 @@ class RibActivityTest { } } + @Test + fun backPress_whenRouterHandles_activityRemainsRunning() { + val activity = Robolectric.buildActivity(BackPressActivity::class.java).setup().get() + activity.setHandlesBackPress(true) + + activity.onBackPressedDispatcher.onBackPressed() + + assertThat(activity.isFinishing).isFalse() + assertThat(activity.unhandledBackPressCount).isEqualTo(0) + } + + @Test + fun backPress_whenRouterDoesNotHandle_callsOnUnhandledBackPressed() { + val activity = Robolectric.buildActivity(BackPressActivity::class.java).setup().get() + activity.setHandlesBackPress(false) + + activity.onBackPressedDispatcher.onBackPressed() + + assertThat(activity.unhandledBackPressCount).isEqualTo(1) + } + + @Test + fun backPress_whenRouterDoesNotHandle_activityFinishes() { + val activity = Robolectric.buildActivity(BackPressActivity::class.java).setup().get() + activity.setHandlesBackPress(false) + + activity.onBackPressedDispatcher.onBackPressed() + + assertThat(activity.isFinishing).isTrue() + } + + @Test + fun backPress_dispatchedViaOnBackPressedDispatcher_routerIsConsulted() { + // Regression guard: back press must be wired through OnBackPressedDispatcher so that + // Predictive Back works. If it were only handled via the deprecated onBackPressed() + // override, triggering the dispatcher would bypass the router entirely. + val activity = Robolectric.buildActivity(BackPressActivity::class.java).setup().get() + activity.setHandlesBackPress(false) + + activity.onBackPressedDispatcher.onBackPressed() + + assertThat(activity.unhandledBackPressCount).isEqualTo(1) + } + companion object { private const val TEST_BUNDLE_KEY = "test_bundle_key" private const val TEST_BUNDLE_VALUE = "test_bundle_value" diff --git a/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt b/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt index 937a93832..664882927 100644 --- a/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt +++ b/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt @@ -184,6 +184,18 @@ constructor( return top.state } + /** + * Returns the router immediately below the current top of the stack, without modifying the stack. + * Returns null if the stack has fewer than two entries. Useful for predictive back animations + * that need to pre-render the previous screen before the navigation commits. + */ + public fun peekPreviousRouter(): Router<*>? { + if (navigationStack.size < 2) return null + val iter = navigationStack.iterator() + iter.next() // skip current (top) + return iter.next().router + } + @IntRange(from = 0) override fun size(): Int { return navigationStack.size From 92442807ca53e242930d074a68302dad4d1f59ea Mon Sep 17 00:00:00 2001 From: georgep Date: Thu, 4 Jun 2026 15:51:45 +0000 Subject: [PATCH 2/6] minsdk and activity bumps --- conventions/src/main/kotlin/ribs.android.application.gradle.kts | 2 +- conventions/src/main/kotlin/ribs.android.library.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conventions/src/main/kotlin/ribs.android.application.gradle.kts b/conventions/src/main/kotlin/ribs.android.application.gradle.kts index 61c606996..130ddd47f 100644 --- a/conventions/src/main/kotlin/ribs.android.application.gradle.kts +++ b/conventions/src/main/kotlin/ribs.android.application.gradle.kts @@ -39,7 +39,7 @@ android { compileSdk = 36 defaultConfig { - minSdk = 21 + minSdk = 23 targetSdk = 35 versionCode = 1 versionName = "1.0" diff --git a/conventions/src/main/kotlin/ribs.android.library.gradle.kts b/conventions/src/main/kotlin/ribs.android.library.gradle.kts index 236304582..46d1aeb7a 100644 --- a/conventions/src/main/kotlin/ribs.android.library.gradle.kts +++ b/conventions/src/main/kotlin/ribs.android.library.gradle.kts @@ -43,7 +43,7 @@ android { compileSdk = 36 defaultConfig { - minSdk = 21 + minSdk = 23 } compileOptions { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0dd406063..f791af92a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] android-api = "4.1.1.4" android-studio = "2023.3.1.20" -androidx-activity = "1.9.0" +androidx-activity = "1.13.0" androidx-annotation = "1.9.1" androidx-appcompat = "1.6.1" androidx-lifecycle = "2.6.2" From 19d76b2bc8d657f1e2e6aea771a5cb8724c23655 Mon Sep 17 00:00:00 2001 From: georgep Date: Mon, 21 Sep 2026 19:31:13 +0000 Subject: [PATCH 3/6] Scope predictive back to core back-navigation support Opt the intellij demo into the predictive back gesture via android:enableOnBackInvokedCallback, so the RibActivity OnBackPressedCallback path is actually exercised on Android 13+. Drop StackRouterNavigator.peekPreviousRouter(). It was added only to let a peek animation pre-render the previous screen, and that animation is not part of this change, so it was unused public API. Co-Authored-By: Claude Opus 5 (1M context) --- demos/intellij/src/main/AndroidManifest.xml | 3 ++- .../kotlin/com/uber/rib/core/StackRouterNavigator.kt | 12 ------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/demos/intellij/src/main/AndroidManifest.xml b/demos/intellij/src/main/AndroidManifest.xml index d7437440d..a1bb55b9c 100644 --- a/demos/intellij/src/main/AndroidManifest.xml +++ b/demos/intellij/src/main/AndroidManifest.xml @@ -13,7 +13,8 @@ android:allowBackup="false" android:icon="@drawable/ub__ic_launcher" android:label="@string/app_name" - android:screenOrientation="portrait"> + android:screenOrientation="portrait" + android:enableOnBackInvokedCallback="true"> diff --git a/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt b/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt index 664882927..937a93832 100644 --- a/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt +++ b/libraries/rib-router-navigator/src/main/kotlin/com/uber/rib/core/StackRouterNavigator.kt @@ -184,18 +184,6 @@ constructor( return top.state } - /** - * Returns the router immediately below the current top of the stack, without modifying the stack. - * Returns null if the stack has fewer than two entries. Useful for predictive back animations - * that need to pre-render the previous screen before the navigation commits. - */ - public fun peekPreviousRouter(): Router<*>? { - if (navigationStack.size < 2) return null - val iter = navigationStack.iterator() - iter.next() // skip current (top) - return iter.next().router - } - @IntRange(from = 0) override fun size(): Int { return navigationStack.size From d99a5a5973646004278c0389147cbfa6a5c62f45 Mon Sep 17 00:00:00 2001 From: georgep Date: Mon, 21 Sep 2026 19:55:06 +0000 Subject: [PATCH 4/6] Opt the stack-nav demo into the predictive back gesture Add android:enableOnBackInvokedCallback to the stack-nav manifest, matching the intellij demo. This demo drives StackRouterNavigator, so it exercises the RibActivity OnBackPressedCallback path against a real router stack rather than a single screen. Co-Authored-By: Claude Opus 5 (1M context) --- demos/stack-nav/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demos/stack-nav/src/main/AndroidManifest.xml b/demos/stack-nav/src/main/AndroidManifest.xml index ecc1c4ada..c74923b42 100644 --- a/demos/stack-nav/src/main/AndroidManifest.xml +++ b/demos/stack-nav/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ android:allowBackup="false" android:icon="@drawable/ub__ic_launcher" android:label="@string/app_name" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:enableOnBackInvokedCallback="true"> From 6de1055c97f6314e910c65a332245e5177be3282 Mon Sep 17 00:00:00 2001 From: georgep Date: Tue, 22 Sep 2026 16:10:58 +0000 Subject: [PATCH 5/6] Dispatch the null-router back test through OnBackPressedDispatcher RibActivity now handles back via an OnBackPressedCallback instead of an onBackPressed() override, so the #662 regression test has to drive the dispatcher to reach it. On androidx.activity 1.13 the deprecated ComponentActivity.onBackPressed() delivers through NavigationEventInput, which never reaches the dispatcher under Robolectric. Intent is unchanged: with attachContent overridden to skip attach, a back press must not crash on the null router and must fall through to super. Co-Authored-By: Claude Opus 5 (1M context) --- .../test/kotlin/com/uber/rib/core/RibActivityTest.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt b/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt index f7cfb2e1e..798690899 100644 --- a/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt +++ b/libraries/rib-android/src/test/kotlin/com/uber/rib/core/RibActivityTest.kt @@ -245,13 +245,15 @@ class RibActivityTest { } @Test - fun onBackPressed_whenRouterIsNull_shouldFallThroughToSuper() { - val activity = Robolectric.buildActivity(RootlessActivity::class.java).create(null).get() + fun backPress_whenRouterIsNull_shouldFallThroughToSuper() { + val activity = Robolectric.buildActivity(RootlessActivity::class.java).setup().get() - activity.onBackPressed() + // Back is dispatched through OnBackPressedDispatcher rather than the deprecated + // onBackPressed(), which on androidx.activity 1.13 routes through NavigationEventInput. + activity.onBackPressedDispatcher.onBackPressed() - // With router == null, the elvis-safe delegation returns null, which is != true, - // so the activity's fallback path runs (unhandled back + super.onBackPressed()). + // With router == null, the callback's delegation returns null, which is != true, so the + // fallback path runs (unhandled back + dispatcher fall-through). assertThat(activity.unhandledBackPressedInvocations).isEqualTo(1) assertThat(activity.isFinishing).isTrue() } From 804234745fbac42c9283dbd152c35d6ff0d991de Mon Sep 17 00:00:00 2001 From: georgep Date: Wed, 23 Sep 2026 18:09:31 +0000 Subject: [PATCH 6/6] cleanup --- CHANGELOG.md | 2 +- PREDICTIVE_BACK.md | 78 ---------------------------------------------- 2 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 PREDICTIVE_BACK.md diff --git a/CHANGELOG.md b/CHANGELOG.md index aade40eef..5483f2474 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Unreleased -* [Android] Support Android Predictive Back gesture. `RibActivity` now registers an `OnBackPressedCallback` instead of overriding the deprecated `onBackPressed()`. No changes required to existing interactors or routers. See [PREDICTIVE_BACK.md](PREDICTIVE_BACK.md) for opt-in instructions and migration details. +* [Android] Support Android Predictive Back gesture. `RibActivity` now registers an `OnBackPressedCallback` instead of overriding the deprecated `onBackPressed()`. No changes required to existing interactors or routers. ### Version 0.1.0 diff --git a/PREDICTIVE_BACK.md b/PREDICTIVE_BACK.md deleted file mode 100644 index 25c8f48fe..000000000 --- a/PREDICTIVE_BACK.md +++ /dev/null @@ -1,78 +0,0 @@ -# Predictive Back Gesture Support - -Android 14 (API 34) introduced the [Predictive Back gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture), which shows a preview animation of where a back swipe will land before the user commits to it. Apps must explicitly opt in and use the modern back-navigation APIs to get these animations. - -## What changed in RIBs - -`RibActivity` previously intercepted back presses by overriding `onBackPressed()`, which is deprecated for back-interception purposes on Android 13+. It now registers an `OnBackPressedCallback` with `onBackPressedDispatcher` instead. - -The internal RIBs back-handling chain is **unchanged**: - -``` -OnBackPressedCallback (in RibActivity) - └─ Router.handleBackPress() - └─ Interactor.handleBackPress(): Boolean -``` - -Interactors that override `handleBackPress()` and `ScreenStackBase` / `StackRouterNavigator` users require **no changes**. - -## Opting in to Predictive Back animations - -To enable the system back animations (swipe-to-home, cross-activity, cross-task), add the following to your app's `AndroidManifest.xml`: - -```xml - -``` - -This flag is what triggers the visual preview animations on Android 14+. Without it, back navigation continues to work exactly as before — the `RibActivity` change is fully backward-compatible regardless of this flag. - -You can also opt individual activities in or out: - -```xml - -``` - -## Custom in-app back animations - -If you want to drive your own animated preview (e.g., a custom route transition) during the back swipe, override `handleOnStarted`, `handleOnProgressed`, `handleOnCancelled`, and `handleOnBackPressed` in a custom `OnBackPressedCallback` and register it **before** the RIBs callback in your activity's `onCreate`: - -```kotlin -class MyActivity : RibActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - // Register custom callback first so it sits ahead of RibActivity's in the chain. - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - // Only intercept when your custom animation applies; otherwise disable - // this callback so RibActivity's callback takes over. - if (shouldAnimateCustomTransition()) { - runCustomBackAnimation() - } else { - isEnabled = false - onBackPressedDispatcher.onBackPressed() - isEnabled = true - } - } - }) - super.onCreate(savedInstanceState) - } -} -``` - -## SDK requirements - -| Requirement | Version | -|---|---| -| `androidx.activity` (transitive via `androidx.appcompat`) | 1.6.0+ | -| Predictive Back animations visible to users | Android 14+ (API 34) | -| `android:enableOnBackInvokedCallback` manifest flag | Android 13+ (API 33) — ignored on older OS versions | - -No changes to your `build.gradle` files are needed. `androidx.appcompat:1.6.x` already provides the required `OnBackPressedCallback` API. - -## Legacy behavior - -Apps that do **not** add `android:enableOnBackInvokedCallback="true"` to their manifest are unaffected. Back navigation continues to work identically to before on all API levels. The change to `RibActivity` is purely internal.