Androidアプリ開発 Fragment バックスタックの仕様と操作を理解する

2017年10月22日(編集2017年10月22日)
このエントリーをはてなブックマークに追加

Androidアプリを作成するとき、Fragmentの管理をバックスタックに任せることが多々あります。
この記事は、Androidアプリでバックスタックを使って、Fragmentを管理する方法を記載した記事です。

環境はAndroid 8.0 (API level 26) です。

環境

  • macOS Sierra
  • android sdk 26
  • Android Studio 2.3.3
  • kotlin 1.1.1

難易度

中級者向け

サンプルコード

Android-Basic-Technique-Demo

バックスタック

スタック構造でFragmentやAcitivtyを扱うことをバックスタックといいます。
Fragmentは、FragmentManagerクラスとFragmentTransactionクラスを使ってバックスタックで管理します。しかし、バグの原因になりやすいので、しっかりとした学習が必要です。

サンプルアプリ設計

メイン画面を扱うFragmentをActivityに重ねます。そして、メインFragmentからさらにFragmentを追加するアプリを作成します。

内容 クラス名
画面の部品やライフサイクルを扱うルートのActivity RootActivity.kt
メイン画面のFragment MainFragment.kt
メイン画面に追加するSubFragment SubFragment.kt
SubFragmentから追加する部品フラグメント PartsFragment.kt

目次

1. バックスタックなしでFragmentを追加

バックスタックを使わないでRootActivityにMainFragmentを追加します。

{project_folder}/RootActivity.kt
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.frag, MainFragment.newInstance(), MainFragment.NAME)
transaction.commitAllowingStateLoss()
         

FragmentTransaction#addメソッドでレイアウトで定義したFrameLayout(R.id.frag)にFragmentを追加します。FragmentTransaction#commitAllowingStateLossでトランザクションをコミットします。

上記のソースコードは、バックスタックにFragmentが追加されません

バックスタックに追加されないことをコードで確認してみましょう。バックスタックの数は以下のように取得します。

{project_folder}/RootActivity.kt
private fun setDisplay() {
    display.text = supportFragmentManager.backStackEntryCount.toString()
}
         

FragmentManager#getBackStackEntryCountメソッドは現在のバックスタックにあるエントリの数を返します。

このメソッドは、FragmentManager.javaで以下のように実装されています。

@Override
public int getBackStackEntryCount() {
    return mBackStack != null ? mBackStack.size() : 0;
}     
         

mBackStackはArrayListクラスです。つまり、getBackStackEntryCountメソッドはBackStackの数を返しています。

上記のコードをボタン押下時に実行して、TextViewに表示します。

fig1. バックスタック取得

BackStackの数が「0」と表示されました。
FragmentTransaction#addメソッドとFragmentTransaction#commitAllowingStateLossメソッドを呼び出すだけでは、トランザクションはバックスタックに積まれません。

2. バックスタックありでFragmentを追加

Fragment追加時に、トランザクションをバックスタックに追加します。

{project_folder}/RootActivity.kt
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.frag, MainFragment.newInstance(), MainFragment.NAME)
// バックスタックに追加
transaction.addToBackStack(MainFragment.NAME)
transaction.commitAllowingStateLoss()
         

FragmentTransaction#addToBackStack()を呼び出すと、commit()を呼び出す前に適用されたすべての変更点が1 つのトランザクションとしてバックスタックに保存されます。

ボタンを押下してバックスタックの状態を確認してみましょう。

fig2. バックスタック取得

BackStackの数が「1」と表示されました。つまり、FragmentTransaction#addToBackStack()でトランザクションがバックスタックに積まれたことが確認できました。

3. バックスタックをポップする

バックスタックに積み上げたトランザクションはスタックからポップすることができます。ポップすると、トランザクションを登録したFragmentの状態に戻すことができます。ポップはスタックからデータを取り出すことです。後に入れたものが先に取り出されます。

{project_folder}/RootActivity.kt
private fun popBackStack() {
    supportFragmentManager.popBackStack()
}
         

重要な箇所なので、FragmentManager.javaのソースコードを読んで処理内容を理解しておきましょう。

@Override
public void popBackStack() {
    enqueueAction(new Runnable() {
        @Override public void run() {
            popBackStackState(mHost.getHandler(), null, -1, 0);
        }
    }, false);
}  
         

非同期でpopBackStackStateメソッドを呼び出しています。

if (name == null && id < 0 && (flags&POP_BACK_STACK_INCLUSIVE) == 0) {
    // something
} 
         

popBackStackメソッドは、popBackStackStateメソッドの上記の条件文を満たします。
「flags&POP_BACK_STACK_INCLUSIVE」は論理演算で 0 & 1 なので0となります。論理演算は 1 & 1 以外は0になります。なので「(flags&POP_BACK_STACK_INCLUSIVE) == 0」はtrueとなります。

int last = mBackStack.size()-1;
if (last < 0) {
    return false;
}
final BackStackRecord bss = mBackStack.remove(last);
SparseArray<Fragment> firstOutFragments = new SparseArray<Fragment>();
SparseArray<Fragment> lastInFragments = new SparseArray<Fragment>();
if (mCurState >= Fragment.CREATED) {
    bss.calculateBackFragments(firstOutFragments, lastInFragments);
}
bss.popFromBackStack(true, null, firstOutFragments, lastInFragments);
reportBackStackChanged();
         

「int last = mBackStack.size()-1;」で最後のバックスタックを取得して、「final BackStackRecord bss = mBackStack.remove(last);」で最後のバックスタックを削除しています。

コードを読むことで、最後に保存したバックスタックが削除されるのが確認できました。

最後に、アプリのボタンを押下してpopBackStack()の処理を確認してみましょう。

fig3. popBackStack

BackStackの数が「0」と表示されました。Fragmentも削除されました。バックスタックからポップされたことが確認できました。

4. バックスタックありでFragmentを追加し、ディバイスバックを押す

Fragment追加時に、トランザクションをバックスタックに追加し、ディバイスのバックボタンを押します。

fig4. ディバイスバック

MainFragmentが削除されて、バックスタックの数が0になります。
つまり、バックスタックが積まれた状態でバックボタンを押下すると、バックスタックを減算する処理になります。
確認のために、onBackPressedのソースコードも読んでみましょう。onBackPressedメソッドが定義されているのはFragmentActivityクラスです。

@Override
public void onBackPressed() {
    FragmentManager fragmentManager = mFragments.getSupportFragmentManager();
    final boolean isStateSaved = fragmentManager.isStateSaved();
    if (isStateSaved && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
        // Older versions will throw an exception from the framework
        // FragmentManager.popBackStackImmediate(), so we'll just
        // return here. The Activity is likely already on its way out
        // since the fragmentManager has already been saved.
        return;
    }
    if (isStateSaved || !fragmentManager.popBackStackImmediate()) {
        super.onBackPressed();
    }
}
         

FragmentManager.popBackStackImmediate()が実行されています。このメソッドは、すぐにpopBackStackを実行します。つまり、バックスタックをポップします。
当然、バックスタックは減算されます。

@Override
public boolean popBackStackImmediate() {
    checkStateLoss();
    executePendingTransactions();
    return popBackStackState(mHost.getHandler(), null, -1, 0);
}
         

FragmentManager.popBackStackImmediate()は実行結果をboolean型で返します。

boolean popBackStackState(Handler handler, String name, int id, int flags) {
    // something
    return true;
}
         

バックスタックがある場合、FragmentManager.popBackStackImmediate()はtrueを返します。なので、「!fragmentManager.popBackStackImmediate()」の結果はfalseになります。バックスタックがない場合はfalseが返ります。なのでsuper.onBackPressed()が呼ばれます。super.onBackPressed()はActivity#finishを呼び出すので、Activityが閉じます。

このケースでは、trueが返ります。
なので、MainFragmentが削除されて、バックスタックの数が0になります。Activityは閉じません。

5. バックススタックとFragmentを追加していく

Fragment追加時に、トランザクションをバックスタックに追加し、さらに別のFragmentとトランザクションを追加します。

fig5. 複数のFragmentとバックスタックを追加

SubFragmentを追加して、「2」が表示されました。バックスタックに2つのトランザクションが溜まっている状態になっています。

ディバイスバックを押します。

fig6. deviceバック

SubFragmentが消えました。バックスタックが1つ消えたためです。数も1になりました。

続いて、popbackstackのボタンを押下します。

fig7. popbackstack

MainFragmentが消えました。バックスタックから取り出されたためです。数も0になりました。

このように、バックスタックを使えば、フラグメントの様々な状態を管理できます。

6. 任意のバックスタックの状態にする

POP_BACK_STACK_INCLUSIVEを使うと、引数に指定した位置までバックスタックをクリアすることができます。また、nullの場合は、すべてのバックスタックをクリアします。

{project_folder}/RootActivity.kt
private fun popBackStackInclusive() {
    supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
         

PartsFragmentを何回も追加してバックスタックを7つ積み上げてから、popBackStackInclusiveを実行します。

fig8. popBackStackInclusive

バックスタックが0になりました。
続いて、MainFragmentの状態まで戻せるように、引数のTAGにSubFragment.NAMEを設定します。
POP_BACK_STACK_INCLUSIVEは、引数に指定した位置まで(含んで)バックスタックをクリアします。

{project_folder}/RootActivity.kt
private fun popBackStackInclusive() {
    supportFragmentManager.popBackStack(SubFragment.NAME, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
         

引数にTAGを指定してINCLUSIVEボタンを押します。

fig9. popBackStackInclusive tag

SubFragment.NAMEの状態まで削除され、バックスタックが1になりました。

FragmentManager.POP_BACK_STACK_INCLUSIVEを指定した時のFragmentManager.javaのコードも確認しておきましょう。

if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) {
    index--;
    // Consume all following entries that match.
    while (index >= 0) {
        BackStackRecord bss = mBackStack.get(index);
        if ((name != null && name.equals(bss.getName()))
                || (id >= 0 && id == bss.mIndex)) {
            index--;
            continue;
        }
        break;
    }
}
         

flags&POP_BACK_STACK_INCLUSIVEが 1 & 1 で1になり、条件を満たすので上記の処理が実行されます。

while文でTAGの名称を探して、indexを減算しています。

for (int i=mBackStack.size()-1; i>index; i--) {
    states.add(mBackStack.remove(i));
}
         

indexの位置までmBackStackを削除しています。stackの先出し処理です。

コードを読むと、POP_BACK_STACK_INCLUSIVEで、引数に指定した位置まで(含んで)バックスタックがクリアされる理由がわかります。

結論

バックスタック便利な機能ですが、慣れないと挙動がわかりにくいです。インターネットで検索しても、間違った説明をしているサイトが結構あります。何度かソースコードを読んで、きちんと仕様を理解しておくことをお勧めします。

関連記事

タグ検索で調べてみよう

Android8.0 kotlin1.1.1 UI