【Android】MenuTabのFragmentの遷移を自前のスタックで管理する
はじめに
概要
仕事の開発の方でタブメニューを使っていて壁にぶつかったので備忘録として残しておきます。
トピックとしては、「Tabメニューの一つのタブで、Fragmentの遷移をさせた時の管理」でぶつかった課題とその解決策について記します。
具体的なエラー
Tabに紐付いたFragmentから、別のFragmentに遷移させ、BackStackに前の状態を積んだ時に、特定ケースで何故かアプリが落ちるといった問題に悩まされました。
成果物
問題解決の成果物としてTabのFragment遷移管理を簡単にするためのモジュールを作ったので公開しておきます。
(お粗末ですが。。。)
使い方はREADMEを見てくれれば。。。
https://github.com/taisho6339/tab_fragment_operation
問題にぶつかったケース
たとえばこういう設計のタブメニューの画面を作ります。
つまり、
・一つのActivityにタブのコンテンツとしてFragmentをぶら下げる。
・さらにそのFragmentから何らかのタイミングで別のFragmentに遷移させる。
・遷移は、Fragmentから一旦Activityにコールバックして、Activityで管理する。
といった仕組み。
ぶつかった問題
- まずFragmentA-1が開いている状態で、FragmentA-2を呼び出す。
- 親のActivity側で、コンテナをFragmentA-2にreplaceする。
- 呼び出す前の状態を保存するためにaddToBackStackを呼ぶ。
- タブを切り替える。
- 再び元のタブに戻る(この時点でFragmentA-2に帰る)
- バックキーを押して、FragmentA-2を呼び出す前の状態に戻す。
- 落ちる。
エラーログは、
java.lang.IllegalStateException: Fragment already added
こんな感じのが・・・
問題の考察
フレームワーク側のTabを管理するコードを読んでみるとわかりますが、
タブを切り替えた時に、タブに紐づけてあるFragmentのインスタンスがnullだと新しく生成し、スタックにaddするという制御を行っています。
つまり、
- 親のActivity側で、コンテナをFragmentA-2にreplaceした時点でFragmentA-1のインスタンスが消える。
- タブを切り替えてからまた元のタブに戻る
- 消えていたFragmentA-1のインスタンスを再度生成
- FragmentA-2をattach
- バックキーを押すと、FragmentA-2を呼びだす前の状態にロールバックするために、FragmentA-1を再生成しスタックに積もうとする
- が、すでに3で蘇らせているのでバッティング(同じバックスタックに同じFragmentのインスタンスは積めない)
という問題っぽい。
解決策
バックキーによるロールバック時、タブの切り替え時にインスタンスを自動で復旧させてることで、バッティングしてしまっているので、
FragmentManagerのBackStackではなく自前でスタックを用意して管理する
ことで回避できるのではないかと考えました。
つまり、それぞれのタブに対してスタックを紐付けておいて、タブそれぞれでFragmentの遷移を管理しようってお話しです。
というわけで汎用的なモジュールを実装してみました。
https://github.com/taisho6339/tab_fragment_operation
仕様
・FragmentからFragmentに遷移した時に、バックキーで元の状態に戻れるようにする。
・タブのルートまで言った時にさらにバックキーを押すとActivityを終了させる。
ようはこれを満たせばいいわけです。
実現方法
・TabInfoというデータクラスを作り、Fragmentを積むスタックを持たせる。
・格タブにTabInfoをタグとしてくっつけておく。(これでTabInfoがタブから取得できる)
・FragmentからFragmentに遷移するときは、このTabInfoの持つスタックに積む(現在表示されているFragmentが一番上にくる)
・バックキーを押したら、スタックをポップして、その時の一番上のFragmentをコンテナに差し込む
・タブ切り替えは切り替え先のタブのスタックの一番上のFragmentをアタッチ
こんな感じ。
Tabを使うActivityで、
@Override public void onBackPressed() { if (backControl()) return; try { super.onBackPressed(); } catch (IllegalStateException e) { e.printStackTrace(); } } private boolean backControl() { TabInfo tabInfo = (TabInfo) getCurrentTab().getTag(); Stack<Fragment> stack = tabInfo.getBackStack(); if (!stack.isEmpty()) { stack.pop(); if (stack.isEmpty()) { finish(); return true; } Fragment newFragment = stack.peek(); mManager.beginTransaction().replace(mFragmentContentId, newFragment).commit(); return true; } return false; }
また、ActionBarのTabを使ったので、TabListenerの実装クラスに、
@Override public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { Stack<Fragment> stack = ((TabInfo) tab.getTag()).getBackStack(); Fragment fragment = null; if (stack.isEmpty()) { fragment = Fragment.instantiate(mActivity, mClass.getName()); ft.add(mContentId, fragment); stack.add(fragment); } else { fragment = stack.peek(); ft.attach(fragment); } } @Override public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { Stack<Fragment> stack = ((TabInfo) tab.getTag()).getBackStack(); if (!stack.isEmpty()) { Fragment fragment = stack.peek(); ft.detach(fragment); } }
詳しくはgithubで。。。