【Android】複数のFragmentがあるActivityでOnBackPressedCallbackを使う

こんにちは。仙台オフィスでアプリ開発を担当しているはんだです。 以前はもっぱらiOSアプリの開発を行っていたのですが、最近はAndroidアプリの開発もやらせてもらってます。

さて、Androidアプリ上で「戻る」ボタンがタップされた際の処理をOnBackPressedCallbackを使って実装したのですが、その際にいくつか苦労した箇所があったので、この記事で紹介させていただきます。

OnBackPressedCallbackとは

activity:1.0.0から使用できるようになったもので、以下のようなコードで「戻る」処理の実装ができます。

val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
    // 「戻る」処理
}

OnBackPressedCallbackが導入される以前にFragmentに「戻る」処理を書くには、interfaceを準備して、Fragmentに処理を書き、Activityで呼び出す・・・というような流れだったかと思いますが、だいぶスッキリ書けますね。

なお、Activityのsuper.onBackPressed()が呼ばれない状態になっていると、OnBackPressedCallbackを登録しても呼び出されないので注意が必要です。

公式のドキュメントはこちらです。

developer.android.com

複数の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を別途準備する必要もないので積極的に利用したいですね。

今回は以上になります。お読みいただきありがとうございました。