はじめに
こちらの記事を参考にさせていただきました。
Androidで非同期処理を実装していると、IllegalStateExceptionというExceptionが発生することがあります。
これは、onSaveInstanceStateより後のタイミングでFragmentTransactionをcommitしていることが原因で発生するものです。
例えば、APIを叩いてそのレスポンスを受けての処理やタイマーで時限式に動く処理など、アプリがバックグランドにある間に処理が走って、ダイアログを表示したり、次のFragmentに遷移したりするケースがこれに相当するかと思います。
回避方針の詳細説明は他にも記事がたくさん出てくるのでそちらに譲りますが、簡単に言うとonPause ~ onResumeの間
でFragmentTransactionをcommitしないようにすればこの問題を回避できます。
この記事ではRxを使って、onPause以降
に走る処理をonResume
まで遅延させる方法について書きます。
実装
class MainActivity : AppCompatActivity() {
private val canCommitFragmentTransaction: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(true)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Observable
.timer(5, TimeUnit.SECONDS)
.delay { canCommitFragmentTransaction.filter { it } }
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
supportFragmentManager.beginTransaction()
.replace(R.id.container, BlankFragment.newInstance())
.commit()
}
}
override fun onPause() {
super.onPause()
canCommitFragmentTransaction.onNext(false)
}
override fun onResume() {
super.onResume()
canCommitFragmentTransaction.onNext(true)
}
}
MainActivityを起動して5秒後にBlanFragmentを作成するシンプルなアプリです。(関係ない部分は省略)
onPause ~ onResume
の間はFragmentTransaction.commit()できないので、canCommitFragmentTransaction
というBehaviorSubjectにfalseを持たせておきます。
この部分はフラグをSubjectにしている点以外は、参考記事と同じです。
では、Rxでどのようにしてタイミングをずらすか。それはdelayを使って実装できます。
.delay { canCommitFragmentTransaction.filter { it } }
ここで使っているのは以下のdelayメソッドです。
public final <U> Observable<T> delay(Function<? super T,? extends ObservableSource<U>> itemDelay)
ドキュメントを読むと、このメソッドは
Returns an Observable that delays the emissions of the source ObservableSource via another ObservableSource on a per-item basis.
だそうです。
また、itemDelayについては、
a function that returns an ObservableSource for each item emitted by the source ObservableSource, which is then used to delay the emission of that item by the resulting ObservableSource until the ObservableSource returned from itemDelay emits an item
となっています。
itemDelayは前段までの処理から流れてくるTを受け取って、ObservableSourceを返すようなFunctionです。このFunctionがObservableSourceを返すまでdelayはストリームを遅延させてくれるということです。
上記の実装では、delayの引数に
{ canCommitFragmentTransaction.filter { it } }
を渡しています。
canCommitFragmentTransactionにtrueが流れてくるまではこのラムダ式はObservableSourceを返さないので、アプリがバックグランドにある間は処理を保留し、FragmentTransaction.commit()をonResume以降にずらすことができるということになります。実装だけみるととても簡単ですね。
FragmentTransaction.commit()をする用事がなくても、例えばポーリング処理などアプリがフォアグラウンドの間だけ定期的にAPIを叩き、バックグランドにある間は保留したい、みたいな場合にも同じ方法で遅延処理を実現することができます。その場合はonResumeでフラグがtrueになるとバックグラウンドで保留されていた処理が一気に流れるので扱いに注意する必要があります。
また、もう一つの注意点として、delayメソッドはソースとなるObservableSourceからのonErrorの通知はすぐさま伝播させるため、エラー時にもdelayを使ってハンドリングしたい場合(エラーが起きたらダイアログを表示するとか)はretryWhenの中とかで使ってあげる必要があります。
実装については以上です。
まとめ
Rxを使うとメソッド一つで遅延処理ができてとても便利ですね。
どなたかの参考になれば幸いです。
また、改善点や間違いなどありましたらコメントいただけると有り難いです。