こんにちは。仙台オフィスでアプリ開発を担当しているはんだです。 以前はもっぱらiOSアプリの開発を行っていたのですが、最近はAndroidアプリの開発もやらせてもらってます。
さて、Androidアプリ上で「戻る」ボタンがタップされた際の処理をOnBackPressedCallbackを使って実装したのですが、その際にいくつか苦労した箇所があったので、この記事で紹介させていただきます。
OnBackPressedCallbackとは
activity:1.0.0
から使用できるようになったもので、以下のようなコードで「戻る」処理の実装ができます。
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) { // 「戻る」処理 }
OnBackPressedCallbackが導入される以前にFragmentに「戻る」処理を書くには、interfaceを準備して、Fragmentに処理を書き、Activityで呼び出す・・・というような流れだったかと思いますが、だいぶスッキリ書けますね。
なお、Activityのsuper.onBackPressed()が呼ばれない状態になっていると、OnBackPressedCallbackを登録しても呼び出されないので注意が必要です。
公式のドキュメントはこちらです。
複数のFragmentがあるActivityでOnBackPressedCallbackを使う
さて、以上のようにOnBackPressedCallback(以下、本文中では「コールバック」)を登録すること自体は簡単にできるのですが、1つのActivityに複数のFragmentが乗っている場合に苦労しました。 例えば、表示するFragmentをタブで切り替えるようなActivityです。
各Fragmentそれぞれでコールバックを登録した場合、表示されていないFragmentのコールバックが呼ばれてしまったり、一度アプリがバックグラウンドになるとその後のコールバックが呼ばれなくなったりしました。
以降、どうやってそれを解決したのかをまとめたいと思います。
OnBackPressedCallbackの有効無効を切り替える
先述の通り、複数のFragmentでコールバックを登録すると、非表示になっているFragmentのコールバックが呼び出されてしまう場合があります。公式ドキュメントにもこのような記載があります。
addCallback() を通じて複数のコールバックを提供できます。その場合、コールバックは、追加した順序と逆の順序で呼び出され、最後に追加したコールバックが、[戻る] ボタンイベントを最初に処理するチャンスを与えられます。たとえば、one、two、three という名前の 3 つのコールバックをこの順序で追加した場合、three、two、one という順序で呼び出されます。
ということなので、表示されているFragmentのコールバックだけが呼び出されるよう、OnBackPressedCallbackのisEnabled
プロパティをFragmentのonHiddenChanged()
のタイミングで切り替えることにしました。
class MyFragment : Fragment() { // Activityを取得する必要があるので、ここでは初期化できない private lateinit var backPressedCallback : OnBackPressedCallback override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { // 「戻る」処理 } } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) // hiddenならcallback無効, hiddenでなければ有効 backPressedCallback.isEnabled = !hidden } }
これで表示されているFragmentのコールバックだけが呼ばれるようになりました。
OnBackPressedCallbackの内容
「戻る」の処理内容ですが、childFragmentManagerにバックスタックがあればそのFragmentに戻り、ない場合はActivityの通常の「戻る」処理を行うようにするため、以下のように実装しました。
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { /* 各Fragmentで戻る前に実行したい処理などあればここに書く*/ if (childFragmentManager.backStackEntryCount > 0) { childFragmentManager.popBackStack() } else { // Fragmentのコールバックを無効にしてから、再度ActivityにonBackPressed()を処理させる this.isEnabled = false activity?.onBackPressed() } }
これでめでたしめでたし・・・と思いきや、「戻る」ボタンでアプリがバックグラウンドにした後に復帰した場合、問題が起きてしまいました。
FragmentのonCreate()が呼ばれなかった場合、上の処理でコールバックのisEnabled
プロパティを falseにしたものがそのままになってしまい、コールバックが動作しなかったようです。
これはonResume()でisEnabled
プロパティを設定することで解決しました。
override fun onResume() { super.onResume() if (this.isVisible) { // バックグラウンドから復帰した際もコールバックが呼ばれるようにする backPressedCallback.isEnabled = true } }
まとめ
内容をまとめると、このようになります。
class MyFragment : Fragment() { // // Activityを取得する必要があるので、ここでは初期化できない private lateinit var backPressedCallback : OnBackPressedCallback override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { /* 各Fragmentで戻る前に実行したい処理などあればここに書く*/ if (childFragmentManager.backStackEntryCount > 0) { childFragmentManager.popBackStack() } else { // Fragmentのコールバックを無効にしてから、再度ActivityにonBackPressed()を処理させる this.isEnabled = false activity?.onBackPressed() } } } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) // hiddenならcallback無効, hiddenでなければ有効 backPressedCallback.isEnabled = !hidden } override fun onResume() { super.onResume() // バックグラウンドから復帰した際もコールバックが呼ばれるようにする if (this.isVisible) { backPressedCallback.isEnabled = true } } }
OnBackPressedCallbackの設定自体は簡単にできますし、従来のようにinterfaceを別途準備する必要もないので積極的に利用したいですね。
今回は以上になります。お読みいただきありがとうございました。