Android

FragmentTransactionに新たに追加されたcommitNow()について

More than 1 year has passed since last update.

Android その3 Advent Calendarの8日目を担当します、kakajikaと申します。
電子書籍ビューアアプリなんかを作ったりしています。

数日前のAndroid その2 Advent CalendarでkikuchyさんによるFragmentに関する記事があったのに触発されて、Advent Calendarに初参戦してみました。Androidを始めたのがFragment全盛期?のAndroid 4.0(Ice Cream Sandwich)の頃だったこともあり、Fragmentとは長い付き合いです :sweat_smile:

記事中にFragmentTransaction#commit()の処理タイミングに関する話があったのですがちょっと情報が古いかなと思ったので、support-v4:24.0.0で追加された比較的新し目のAPIであまり情報を見かけないFragmentTransaction#commitNow()について、動作検証も交えつつ紹介しようかと思います。

なお検証に使用したサポートライブラリは現時点で最新の25.0.1です。

まず、公式Javadocによる説明

https://developer.android.com/reference/android/support/v4/app/FragmentTransaction.html#commitNow()

Commits this transaction synchronously. Any added fragments will be initialized and brought completely to the lifecycle state of their host and any removed fragments will be torn down accordingly before this call returns.

commitNow()というメソッド名の通りなのですが、同期的にトランザクション処理を行ってくれるようです。

Committing a transaction in this way allows fragments to be added as dedicated, encapsulated components that monitor the lifecycle state of their host while providing firmer ordering guarantees around when those fragments are fully initialized and ready. Fragments that manage views will have those views created and attached.

また、ホストのライフサイクルを監視し適切なタイミングでViewを生成してアタッチしてくれるようです。

これだけではどう動くのかいまいちわからないので、実際に動かして検証してみましょう。

実際どんな動きをするのか

次のようなコードで検証します(layoutにはすでにFragment1が追加されているものとします)

Activity.kt
log("Transaction: begin")
supportFragmentManager
        .beginTransaction()
        .replace(R.id.layout, Fragment2())
        .commitNow()
log("Transaction: end")

Fragmentのライフサイクルをログに出力してみます

Logcat
Transaction: begin
Fragment2: onAttach
Fragment2: onCreate
Fragment1: onPause
Fragment1: onStop
Fragment1: onDestroyView
Fragment2: onCreateView
Fragment2: onViewCreated
Fragment2: onActivityCreated
Fragment2: onStart
Fragment2: onResume
Transaction: end
Fragment1: onDestroy
Fragment1: onDetach

commitNow()が終わった時点でFragmentの入れ替えが済んでいるのがわかりますね! :smile_cat:

また、ActivityのonCreate()の中でcommitNow()を呼んだ場合はこうなります

Logcat
Transaction: begin
Fragment2: onAttach
Fragment2: onCreate
Fragment1: onDestroy
Fragment1: onDetach
Transaction: end
Fragment2: onCreateView
Fragment2: onViewCreated
Fragment2: onActivityCreated
Fragment2: onStart
Fragment2: onResume

Activity側の準備が終わるまではViewの生成や追加ができないので、onCreateView()以降は後回しにしてくれるようです。

いずれにせよcommitNow()が終わった時点で少なくともonAttach()onCreate()まで完了していることがわかります。commitNow()の直後にFragmentManager#findFragmentById()なんかを呼んだ場合もちゃんと最新のものが取得できます。

今までFragmentの追加されるタイミングに頭を悩ませてきた方々にとっては福音ですね。 :innocent:

childFragmentManagerを使用した場合

ちょっと複雑になりますがレイアウトが入れ子になっている場合を考慮して、Fragment2のonCreate()の中でchildFragmentManagerを使用してChildFragmentを追加してみます

Fragment2.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    log("TransactionChild: begin")
    childFragmentManager
            .beginTransaction()
            .replace(R.id.layout_inner, ChildFragment())
            .commitNow()
    log("TransactionChild: end")
}
Logcat
Transaction: begin
Fragment2: onAttach
Fragment2: onCreate
    TransactionChild: begin
    ChildFragment: onAttach
Fragment2: onAttachFragment
    ChildFragment: onCreate
    TransactionChild: end
Fragment1: onPause
Fragment1: onStop
Fragment1: onDestroyView
Fragment2: onCreateView
Fragment2: onViewCreated
Fragment2: onActivityCreated
    ChildFragment: onCreateView
    ChildFragment: onViewCreated
    ChildFragment: onActivityCreated
Fragment2: onStart
    ChildFragment: onStart
Fragment2: onResume
    ChildFragment: onResume
Transaction: end
Fragment1: onDestroy
Fragment1: onDetach

こちらもchildFragmentManagerのcommitNow()が終わった時点でChildFragmentが追加されていて、問題なさそうです。ライフサイクルも親のFragmentにきちんとbindされているのがわかります。

おまけ: FragmentTransaction#commit()だと…

一応、commitNow()の代わりにcommit()を使用した場合のログはこうなりました

Logcat
Transaction: begin
Transaction: end
Fragment2: onAttach
Fragment2: onCreate
Fragment1: onPause
Fragment1: onStop
Fragment1: onDestroyView
Fragment2: onCreateView
Fragment2: onViewCreated
Fragment2: onActivityCreated
Fragment2: onStart
Fragment2: onResume
Fragment1: onDestroy
Fragment1: onDetach

Handlerにpostして処理を後回しにするので当然ですね。
もちろん、commit()の直後にFragmentManager#findFragmentById()を呼んだりしても期待した結果は返ってきません。

executePendingTransactions()と比較した利点

Javadocより

Calling commitNow is preferable to calling commit() followed by executePendingTransactions() as the latter will have the side effect of attempting to commit all currently pending transactions whether that is the desired behavior or not.

以前より、commit()を行った後にexecutePendingTransactions()を呼んでトランザクションの実行を待機するという手法がありましたが、この方法では自身とは関係ないすべてのトランザクションについてもその場で実行してしまうため、その点で自身だけを即座に実行するcommitNow()の方が優れているよ、とのことです。

注意すべき点

commit()とは併用しないのがよさそう

同期的に処理するcommitNow()に対して、commit()はHandlerに処理をpostするため、2つを連続して使った場合などに順番通りに処理されないといった問題が生じます。

例えば次のようなコードを実行すると、

Activity.kt
log("Transaction1: begin")
supportFragmentManager
        .beginTransaction()
        .replace(R.id.layout, Fragment2())
        .commit()
log("Transaction1: end")

log("Transaction2: begin")
supportFragmentManager
        .beginTransaction()
        .replace(R.id.layout, Fragment3())
        .commitNow()
log("Transaction2: end")
Logcat
Transaction1: begin
Transaction1: end
Transaction2: begin
Fragment3: onAttach
Fragment3: onCreate
Fragment1: onPause
Fragment1: onStop
Fragment1: onDestroyView
Fragment3: onCreateView
Fragment3: onViewCreated
Fragment3: onActivityCreated
Fragment3: onStart
Fragment3: onResume
Transaction2: end
Fragment2: onAttach
Fragment2: onCreate
Fragment3: onPause
Fragment3: onStop
Fragment3: onDestroyView
Fragment2: onCreateView
Fragment2: onViewCreated
Fragment2: onActivityCreated
Fragment2: onStart
Fragment2: onResume
Fragment1: onDestroy
Fragment1: onDetach
Fragment3: onDestroy
Fragment3: onDetach

このように2つ目の処理が先に行われる結果となり、画面にはFragment2が表示されてしまいました。

今後サポートライブラリ側が修正される可能性もありますが、commitNow()を使う場合には同じターゲットに対してcommit()は使わないようにするのが無難そうです。

BackStackは使えない

Transactions committed in this way may not be added to the FragmentManager's back stack, as doing so would break other expected ordering guarantees for other asynchronously committed transactions. This method will throw IllegalStateException if the transaction previously requested to be added to the back stack with addToBackStack(String).

JavaDocでも言及されていますが、やはりcommit()による処理が絡むと順番の保証ができなくなってしまうため、BackStackは使用できなくなっているようです。

まとめ: commitNow便利!

以上、Fragmentの困りポイントを一つ解消してくれる(かもしれない)commitNow()の紹介でした!

実際にプロジェクトでも使っていますがFragment切替時の処理の流れが分かりやすくなり、かなり便利に感じました。

初Advent Calendarで拙い記事でしたが、少しでもみなさんのお役に立てれば幸いです。