読者です 読者をやめる 読者になる 読者になる

情報系の凡才日記

エンジニアりんぐの試行錯誤とか参加したイベントとかについて書いていこうとおもいます。

【Android】MenuTabのFragmentの遷移を自前のスタックで管理する

はじめに

概要

仕事の開発の方でタブメニューを使っていて壁にぶつかったので備忘録として残しておきます。
トピックとしては、「Tabメニューの一つのタブで、Fragmentの遷移をさせた時の管理」でぶつかった課題とその解決策について記します。

具体的なエラー

Tabに紐付いたFragmentから、別のFragmentに遷移させ、BackStackに前の状態を積んだ時に、特定ケースで何故かアプリが落ちるといった問題に悩まされました。

成果物

問題解決の成果物としてTabのFragment遷移管理を簡単にするためのモジュールを作ったので公開しておきます。
(お粗末ですが。。。)
使い方はREADMEを見てくれれば。。。
https://github.com/taisho6339/tab_fragment_operation

問題にぶつかったケース

たとえばこういう設計のタブメニューの画面を作ります。

f:id:taisho6339:20140701214608p:plain

つまり、


・一つのActivityにタブのコンテンツとしてFragmentをぶら下げる。
・さらにそのFragmentから何らかのタイミングで別のFragmentに遷移させる。
・遷移は、Fragmentから一旦Activityにコールバックして、Activityで管理する。

といった仕組み。

ぶつかった問題

  1. まずFragmentA-1が開いている状態で、FragmentA-2を呼び出す。
  2. 親のActivity側で、コンテナをFragmentA-2にreplaceする。
  3. 呼び出す前の状態を保存するためにaddToBackStackを呼ぶ。
  4. タブを切り替える。
  5. 再び元のタブに戻る(この時点でFragmentA-2に帰る)
  6. バックキーを押して、FragmentA-2を呼び出す前の状態に戻す。
  7. 落ちる。

エラーログは、

java.lang.IllegalStateException: Fragment already added

こんな感じのが・・・

問題の考察

フレームワーク側のTabを管理するコードを読んでみるとわかりますが、

https://code.google.com/p/android/issues/attachmentText?id=40035&aid=400350000000&name=FragmentTabHost.java&token=WHVg3x7dbNNznLcPPSwWxnin_X0%3A1367343846708

タブを切り替えた時に、タブに紐づけてあるFragmentのインスタンスがnullだと新しく生成し、スタックにaddするという制御を行っています。
つまり、

  1. 親のActivity側で、コンテナをFragmentA-2にreplaceした時点でFragmentA-1のインスタンスが消える。
  2. タブを切り替えてからまた元のタブに戻る
  3. 消えていたFragmentA-1のインスタンスを再度生成
  4. FragmentA-2をattach
  5. バックキーを押すと、FragmentA-2を呼びだす前の状態にロールバックするために、FragmentA-1を再生成しスタックに積もうとする
  6. が、すでに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で。。。