Closed2

OnBackPressedDispatcher#hasEnabledCallbacks()を使ってハマった話

ピン留めされたアイテム
Hideki HamadaHideki Hamada

概要

  • OnBackPressedDispatcher#hasEnabledCallbacks()の結果が同期的じゃないから安心して使えへんという状況

問題が起きたコード

OnBackPressedDispatcherに登録されているコールバックを優先して処理して、コールバックが存在しない場合は独自に管理しているバックスタックを処理したかったので次のような実装を行った。

class MainActivity : AppCompatActivity()
    // ...

    override fun onBackPressed() {	
        when (onBackPressedDispatcher.hasEnabledCallbacks()) {	
            true -> super.onBackPressed()	
            false -> if (!独自に管理しているバックスタックをpopする処理()) finish()
        }	
    }
}

独自に管理しているバックスタックについてもすべて処理を終えてからActivityが終了することを期待していたが、処理されずにActivityが終了してしまう問題が起きた。

期待していた動作

アプリのコードで明示的にコールバックを登録していない場合は以下のような動作を期待。

  1. onBackPressedDispatcher.hasEnabledCallbacks()はfalseを返す
  2. false -> if (!独自に管理しているバックスタックをpopする処理()) finish() が実行される
    • 独自に管理しているバックスタックが空でなければpop、空ならActivityを終了

実際の動作

アプリのコードで明示的にコールバックを登録していない場合であっても、FragmentManagerが自身のOnBackPressedCallbackを一時的に有効にするケースがあり、タイミング次第で以下のような動作になる。

  1. FragmentTransaction#commitAllowingStateLoss()でFragmentをremove
    • (※非同期のAPI実行中にローディングとして表示していたFragmentを消す)
  2. Activity#onBackPressed()を実行
    • (※APIの実行後に詳細画面から一覧画面に戻るため)
  3. 「問題が起きたコード」でonBackPressedDispatcher.hasEnabledCallbacks()がtrueを返す
    • FragmentManagerに保留の処理が残っている場合のみ
    • androidxの該当コード のコメントにこの動作が記載されている
  4. 「問題が起きたコード」でtrue -> super.onBackPressed()が実行され、FragmentManager#handleOnBackPressedが呼び出される
  5. FragmentManager#handleOnBackPressedでまずFragmentに関する保留の処理がすべて実行され、バックスタックも空なので一時的に有効になっていたコールバックが無効になる
  6. 実行しようとしていたコールバックが無効になったので次の有効なコールバックにフォールバックされる
  7. 結果として「問題が起きたコード」のfalse -> if (!独自に管理しているバックスタックをpopする処理()) finish()は実行されずにActivityが終了する

問題を回避するために実装したコード

Activity#onPressed()のoverrideはやめて、コールバックを登録するようにした。

class MainActivity : AppCompatActivity()
    // ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (!独自に管理しているバックスタックをpopする処理()) finish()
            }
        })
}
このスクラップは2021/06/13にクローズされました