Handling back press in Android 13, the correct way
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
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,
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.
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,
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?
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 FragmentManager
will 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 onBackPressedCallback
s 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 onBackPressedCallback
s).
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
.
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 onBackPressedCallback
s 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.
A complete implementation that uses onBackPressed()
for versions lower than 13 & onBackInvokedDispatcher
for version above 13 will look like this,
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.
Hope you like this discussion, if you’ve any doubts or concerns let me know through comments or Twitter :)