Blog Infos
Author
Published
Topics
Published

Android is known for many things, but recently looking at the platform & API releases it is mainly known for its breaking changes. Considering how they notoriously broke clipboard monitoring functionality (OnPrimaryClipChangedListener) to be available only for system apps starting from Android Q breaking over tons of apps which used to rely on this callback are now deprecated & do not even show up on Google Play searches making this issue tracker one of the most long-running (marked P1) stale request. You can literally see in the comments where developers of many popular apps are requesting for an alternative solution like maybe putting this change/feature behind a Special Permission like System Overlay, etc.

OnBackPressedDispatcher was introduced somewhere around 2019 through androidx.activity API. This change suggested moving away from onBackPressed callback to a new listener method similar to ActivityResultContract. Some said this was a good change as this made us move away from the activity override method & rely on callbacks through listeners which means we can now write the back press logic anywhere like in Fragments. The only catch is the way it handles the callback.

The underlying problem
// for fragment: requireActivity().onBackPressedDispatcher
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// handle logic
if (shouldNotInvokeAgain) {
this.isEnabled = false
}
}
})
view raw backpress-1.kt hosted with ❤ by GitHub

To listen for the back press you add a OnBackPressedCallback. If you look closely the callback accepts a constructor parameter enabled which says whether this callback is active i.e should handledOnBackPressed() method of this callback to be called or not. When you add a callback it creates a Cancelable version of this callback that has a cancel method which removes it from the back press callback list.

There is another overload which accepts LifecycleOwner as an argument that basically calls this cancel method whenever the owner’s state reaches onStop & re-adds the callback when the state reaches onStart. This means you don’t have to handle the removal & adding of callback manually.

All the callbacks added through addCallback will execute in the reverse order, if one is handled then it ignores the rest in the chain. The important part here is that whenever your logic execution is complete you must set the isEnabled property to false or remove() the callback so that it can be skipped next time in the chain. This is what onBackPressedDispatcher does internally,

class OnBackPressedDispatcher ... {
...
@MainThread
public void onBackPressed() {
Iterator<OnBackPressedCallback> iterator =
mOnBackPressedCallbacks.descendingIterator();
while (iterator.hasNext()) {
OnBackPressedCallback callback = iterator.next();
if (callback.isEnabled()) {
callback.handleOnBackPressed();
return;
}
}
if (mFallbackOnBackPressed != null) {
mFallbackOnBackPressed.run();
}
}
...
}

If there are no callbacks to handle the back event then it defaults to use mFallbackOnBackPressed. For androidx.activity.ComponentActivity it will call super.onBackPressed() which means the overridden method of onBackPressed defined in ComponentActivity will be not called instead it will delegate to platform’s app.Activity method. So if you extend ComponentActivity & write a custom back press logic by overriding onBackPressed() method, it won’t be called.

This will only happen when you opt-in to the predictive back gesture introduced in Android 13. The way this work is your window’s DecorView receives a KEYCODE_BACK which then dispatches a key event to app.Activity’s onKeyUp() that then calls the onBackPressed() method. This delivery of events is done by InputStage , more specifically an implementation of InputStage by name ViewPreImeInputStage. If you look at line 6271 of ViewRootImpl, you will see exactly how it dispatches the event & then trace it down to Activity’s onKeyUp() method.

For Android 13, when you opt-in to the predictive back gesture by setting android:enableOnBackInvokedCallback to true in <application> tag, NativePreImeInputStage is used. Here, a special implementation of OnBackInvokedDispatcher called WindowOnBackInvokedDispatcher’s latest onBackInvokedCallback is invoked instead of dispatching the event to Activity.

// 1.
class NativePreImeInputStage ... {
...
@Override
protected int onProcess(QueuedInputEvent q) {
...
// If the new back dispatch is enabled, intercept KEYCODE_BACK before it reaches the
// view tree or IME, and invoke the appropriate {@link OnBackInvokedCallback}.
if (isBack(event) ... ) {
OnBackInvokedCallback topCallback = getOnBackInvokedDispatcher().getTopCallback(); // check below code
if (event.getAction() == KeyEvent.ACTION_UP) {
if (topCallback != null) {
topCallback.onBackInvoked(); // <-- Called here
return FINISH_HANDLED;
}
}
}
...
}
...
}
// 2.
class WindowOnBackInvokedDispatcher ... {
...
// This method is only available in WindowOnBackInvokedDispatcher & it returns the
// latest `onBackInvokedCallback` registered through `registerOnBackInvokedCallback`.
public OnBackInvokedCallback getTopCallback() {
if (mAllCallbacks.isEmpty()) {
return null;
}
for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) {
ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
if (!callbacks.isEmpty()) {
return callbacks.get(callbacks.size() - 1);
}
}
return null;
}
...
}

This registerOnBackInvokedCallback() accepts a priority argument which tells the order in which callbacks will be returned. Here in getTopCallback() it returns the latest onBackInvokedCallback. From Android 13, the app.Activity will register a PRIORITY_SYSTEM callback during onCreate(). This callback does nothing much but mostly finishes/minimizes the activity.

Things started to break when it stopped calling Activity’s onBackPressed()method. This was a major drawback because now we cannot perform any action during the back press before the activity is finished. Earlier we could do that like,

override fun onBackPressed() {
// perform some action
super.onBackPressed();
}
view raw backpress-3.kt hosted with ❤ by GitHub

This, super.onBackPressed() was an important call to all the parents which in turn finishes the activity. Also, make sure to not call finish() anywhere in your OnBackPressedCallback as it will break the predictive back gesture feature.

Supporting Predictive back gesture

So how do we handle such a scenario?

class MainActivity ... {
override fun onCreate(...) {
// This will work but has a catch. Do not implement this,
onBackPressedDispatcher.addCallback(object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (shouldFinish()) {
this.remove()
onBackPressed()
onBackPressedDispatcher.addCallback(this)
}
}
})
}
}
view raw backpress-4.kt hosted with ❤ by GitHub

The above change will work but any other callbacks added after this will be given more priority. From what I’ve seen,androidx.fragment ‘s 1.3.6 version FragmentManager automatically registers an onBackPressedCallback to remove the current fragment from the backstack which means during the back press, the callback which we register will not be called & instead the callbacks registered by FragmentManagerwill be called. So if you are handling fragment transactions manually or using a 3rd party library where there is a need to keep track of such current fragments then you may need to update the code accordingly.

The better way in my opinion is to basically avoid onBackPressedDispatcher & rely on onBackInvokedDispatcher. As you might know, each onBackPressedDispatcher has a onBackInvokedDispatcher. This onBackInvokedDispatcher is responsible for calling all the onBackPressedCallbacks in descending order (check the 2nd code snippet of this article) & from the 3rd snippet, we know the latest onBackInvokedCallback which is registered with a high priority will be invoked (in the default case it will be the one that calls onBackPressedCallbacks).

What we can do is provide our custom implementation with much higher priority. Any priority greater than 0 is fine as that is the default priority with which the default onBackInvokedCallback is registered by onBackInvokedDispatcher.

onBackInvokedDispatcher.registerOnBackInvokedCallback(1000, object: OnBackInvokedCallback {
override fun onBackInvoked() {
// perform any action
...
// finally call the remaining callbacks or just `onBackPressed()` both are same.
// if no callback is registered it will fallback to app.Activity -> onBackPressed()
onBackPressedDispatcher.onBackPressed()
}
})
view raw backpress-5.kt hosted with ❤ by GitHub

The implementation is straightforward, after performing the action we just delegate to onBackPressed() -> onBackPressedDispatcher.onBackPressed() -> android.app.Activity.onBackPressed() as there is no way to directly callapp.Activity ‘s onBackPressed() from a deeply-nested class because that’s how inheritance work.

Make sure there are no onBackPressedCallbacks present in onBackPressedDispatcher. If this contract is followed then you can treat onBackPressedDispatcher.onBackPressed() as your super.onBackPressed() which will finish/minimize the activity.

Conclusion

Make sure to opt-in to the predictive back gesture through the manifest. If your targetSdk is 34, then you can skip the below step.

<application
android:enableOnBackInvokedCallback="true"
...
>
...
</application

A complete implementation that uses onBackPressed() for versions lower than 13 & onBackInvokedDispatcher for version above 13 will look like this,

import android.app.Activity
import android.os.Build
import android.window.OnBackInvokedCallback
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
private typealias BackPressCallback = () -> Boolean
@Suppress("DEPRECATION")
open class BackPressCompatActivity : AppCompatActivity() {
private var listener: BackPressCallback? = null
/**
* A custom back press listener to support predictive back navigation
* introduced in Android 13.
*
* If return true, activity will finish otherwise no action taken.
*/
fun setOnBackPressListener(block: BackPressCallback) {
listener = block
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(1000, object: OnBackInvokedCallback {
override fun onBackInvoked() {
if (block()) {
onBackPressedDispatcher.onBackPressed()
}
}
})
}
}
@Deprecated("Deprecated in Java")
final override fun onBackPressed() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (listener?.invoke() == false) {
return
}
}
super.onBackPressed()
}
}

All you have to do now is extend your Activity class with BackPressCompatActivity and call setOnBackPressListener to either return true to false. Returning true will finish/minimize the activity.

class MainActivity : BackPressCompatActivity() {
override fun onCreate(...) {
setOnBackPressListener { handleBackPress() }
...
}
private fun handleBackPress(): Boolean {
// perform action
...
if (shouldClose()) {
return true
}
return false
}
}

Hope you like this discussion, if you’ve any doubts or concerns let me know through comments or Twitter 🙂

This article was previously posted on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Bringing Android 13 to Tesla vehicles with Flutter Web

Did you know that you can use your favorite Android apps while driving your Tesla? Join me in this session to learn how the Tesla Android Project made it all possible with Flutter Web on…
Watch Video

Bringing Android 13 to Tesla vehicles with Flutter Web

Michał Gapiński
Senior Software Engineer
HappyByte

Bringing Android 13 to Tesla vehicles with Flutter Web

Michał Gapiński
Senior Software Engi ...
HappyByte

Bringing Android 13 to Tesla vehicles with Flutter Web

Michał Gapińsk ...
Senior Software Engineer
HappyByte

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
While Targeting Android 13 , OnbackPressed Override Function is deprecated😢. Usually, we used to…
READ MORE
blog
Today, We will explore the New notification 🔔 runtime permission that was added in…
READ MORE
blog
In this article, we will explore Android 13’s new clipboard UI, as well as…
READ MORE
blog
App launcher icons, the very first interaction that someone has with your app is…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu