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 | |
} | |
} | |
}) |
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(); | |
} |
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) | |
} | |
} | |
}) | |
} | |
} |
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
.
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() | |
} | |
}) |
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.
<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