Androidアプリがバックグラウンドの時にDialogFragmentを表示しようとして落ちる時に試したこと

# 概要
- アプリがバックグラウンド時にFragmentを追加・切替をすると落ちる・
- 回避する一つの方法として、
    1. 親のFragmentがアタッチされているか判定。
    1. `FragmentTransaction#commitAllowingStateLoss()`を使用してFragmentの状態を無視してcommitする。

:::note warn
Fragmentの状態を無視して実行しているのでonSaveInstanceStateを気にしている場合はこの限りではないです。
:::

# 詳細
アプリを使っていると非同期でデータを取得したタイミングで何か事を起こすことがあると思います。
私はよくそのタイミングでDialogFragmentを使っているのですが（これがよくない原因ですが）、DialogFragmentに限らずFragment全般に言えることで、アプリがバックグラウンド時にFragmentを追加や切替などを行うと大体`llegalStateException`が発生します。
原因は後述する[スタックトレース](#起きたエラーのスタックトレース)を見てもらうとわかるのですが、`Fragment#onSaveInstanceState`を通った後にcommitを行うと「セーブしたのに操作しないでよ！！」って怒られてクラッシュします。
回避する方法としては[こういうやり方](https://stackoverflow.com/a/25706263)もあるようですが、自分の場合、Activityに保存されたものを再利用しないので無理矢理`FragmentTransaction#commitAllowingStateLoss()`を使用してDialogFragmentを表示してます。

# 私が回避した方法
``` kotlin
// fragment -> DialogFragmentを呼び出し元のFragment
// fragmentがActivityにアタッチされてるか判定
if (fragment.isAdded) {
  // fragment.show(fragment.childFragmentManager, fragment.tag)
  fragment.childFragmentManager.beginTransaction()
    .add(dialogFragment, fragment.tag)
    .commitAllowingStateLoss()  // Activityの保存状態を気にせずにcommitする
}
```
これで一応非同期処理が終わったタイミングでアプリがバックグラウンドにいてもクラッシュすることはなくなるはずです。
## 試したけど駄目だったパターン
``` kotlin
if (fragment.isAdd)
  fragment.show(fragment.childFragmentManager, fragment.tag)

// もしくは

if (!fragment.isStateSaved)
  fragment.show(fragment.childFragmentManager, fragment.tag)
```
アプリがバックグラウンドかどうか判定してもらってバックグラウンドなら表示しなくていいかって思ったけど甘くなかった。この処理はまずい。
結局Activityの状態に関係なくcommitさせる`FragmentTransaction#commitAllowingStateLoss()`を利用するに落ち着きました。

# 終わりに
他にいい方法もあるとは思うのですが、ViewModelから操作しているという面倒パターンを組んでしまっているので、これが一番手っ取り早いかなって思います。
てか、Firebase Realtime Databaseでデータ量の少ないものをフェッチしてるから、ほぼほぼフォアグラウンドで動作するんですが、たまに（発生率は0.05～0.1%くらいで）バックグラウンドで動作しちゃうので対策したほうがいいなって修正した次第です。
~~これで問題あればまた考えなきゃいけないのは正直めんどくさい。~~

# 起きたエラーのスタックトレース
```
Fatal Exception: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
       at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:1844)
       at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1884)
       at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:329)
       at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:294)
       at androidx.fragment.app.DialogFragment.show(DialogFragment.java:260)
       at com.sample.sample.ui.main.MainViewModel$checkLatest$1.onDataChange(MainViewModel.java:203)
       at com.google.firebase.database.Query$1.onDataChange(Query.java:191)
       at com.google.firebase.database.core.ValueEventRegistration.fireEvent(ValueEventRegistration.java:75)
       at com.google.firebase.database.core.view.DataEvent.fire(DataEvent.java:63)
       at com.google.firebase.database.core.view.EventRaiser$1.run(EventRaiser.java:55)
       at android.os.Handler.handleCallback(Handler.java:873)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loop(Looper.java:193)
       at android.app.ActivityThread.main(ActivityThread.java:6801)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
```

# 参考
- [On showing dialog I get "Can not perform this action after onSaveInstanceState"](https://stackoverflow.com/questions/15729138/on-showing-dialog-i-get-can-not-perform-this-action-after-onsaveinstancestate)
- [commitAllowingStateLossを軽く解説](https://qiita.com/ken-maki/items/ed88d35c0a9b8e728dd5)