こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。
今回私が担当している「auサービスToday」で OnBackInvokedDispatcher を導入したため、その内容をお伝えします。
背景
Android でのバックキーイベントは Activity#onBackPressed()
があり、私を含め Android アプリ開発者は長く慣れ親しんだ API であると思います。しかし、 API レベル 33 で deprecated になり、多くの開発者に衝撃を与えました。
developer.android.com
OnBackInvokedDispatcher について
Activity#onBackPressed()
の代わりに onBackInvokedDispatcher
の使用が推奨されています。
onBackPressed()
とは違い、バックキーイベントを受け取るには registerOnBackInvokedCallback()
メソッドで OnBackInvokedCallback
のバックキーイベントを登録させる必要があります。
しかし、単純にフィールド変数等に定義してしまうと以下のエラーが出てしまいます。
java.lang.NoClassDefFoundError: Failed resolution of: Landroid/window/OnBackInvokedCallback;
OnBackInvokedCallback
は API レベル 33 以上でないと参照ができず、それ未満の端末では実行することができませんでした。
そこでもう一つのバックキーイベントである onBackPressedDispatcher
を用いて、API レベル 33 以上と未満とで処理を分けられるようにしたいと考えました。
onBackPressedDispatcher
は lifecycle に紐づいて使用するバックキーイベントで、例えば Fragment 単位でバックキーイベントのコールバックを受け取ることができます。「auサービスToday」はまだ Fragment を用いて画面表示を行なっているため、onBackPressedDispatcher
のみの対応でも問題はありません。しかし現在フル Compose 化と Navigation 導入を検討しており、 Fragment を使用しなくなることを考えると onBackInvokedDispatcher
を主軸とした設計の方が良いと判断しました。
実装
下記が今回のバックキーイベントをハンドリングしてくれる基本クラスの BackPressHandler
です。
class BackPressHandler( private val activity: FragmentActivity?, private val onBackPressed: () -> Unit ) { private val launcher: BackPressedLauncher by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { BackPressedLauncherTiramisuImpl(activity, ::onBackPressed) } else { BackPressedLauncherImpl(activity, ::onBackPressed) } } fun register() { launcher.register(activity) } fun unregister() { launcher.unregister() } abstract class BackPressedLauncher { abstract fun register(lifecycleOwner: LifecycleOwner) abstract fun unregister() } /** * OS13以上の場合は onBackInvokedDispatcher を用いたバックキーハンドリングをする必要がある * */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) private class BackPressedLauncherTiramisu( private val activity: ComponentActivity?, onBackPressed: () -> Unit ) : BackPressedLauncher() { private val backInvokedCallback = OnBackInvokedCallback { onBackPressed.invoke() } override fun register(lifecycleOwner: LifecycleOwner) { activity?.onBackInvokedDispatcher?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_OVERLAY, backInvokedCallback) } override fun unregister() { activity?.onBackInvokedDispatcher?.unregisterOnBackInvokedCallback(backInvokedCallback) } } /** * OS13未満の場合のバックキーハンドリング * ライブラリで onBackInvokedDispatcher を参照できないため、クラスを分ける */ private class BackPressedLauncherImpl( private val activity: ComponentActivity?, private val onBackPressed: () -> Unit ) : BackPressedLauncher() { private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { onBackPressed.invoke() } } override fun register(lifecycleOwner: LifecycleOwner) { activity?.onBackPressedDispatcher?.addCallback(lifecycleOwner, onBackPressedCallback) } override fun unregister() { onBackPressedCallback.remove() } } }
BackPressedLauncher
の interface を用意し、それを継承した内部クラスとして API レベル 33 以上の BackPressedLauncherTiramisuImpl
とそれ未満の BackPressedLauncherImpl
を作成します。それぞれの内部クラス内で OnBackInvokedCallback
と OnBackPressedCallback
を作成し、登録・解除ができるようにします。内部クラスに内包されているため、 API レベル 33 未満でも OnBackInvokedCallback
が参照できないエラーが発生されなくなります。
こちらを使用する実装としては、例えば Activity では下記のようになります。
val backPressHandler = BackPressHandler(this) { // do back event } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) backPressHandler.register() ~省略~ } override fun onDestroy() { backPressHandler.unregister() ~省略~ }
使用する側は API レベルを意識せずにバックキーイベントを受けとることができるようになりました。
今回は省略しましたが「複数の Fragment を重複して表示をしている場合」や「バックキー無効の場合」は、今回の実装では不十分であるため、通知をしている onBackPressed
等で対応が必要となります。
まとめ
OnBackInvokedDispatcher の導入についてお伝えしました。API レベル 33 未満での考慮が必要であり、またバックキーイベントはアプリ開発の中でも根幹部分の一つでもあるため、しっかりとした理解が必要です。長く開発を共に過ごした onBackPressed API との別れは名残惜しいですが、新たな開発ステージへステップアップしていきましょう。