FragmentTransaction.addToBackStack() を使って Fragment をバックスタックに移動すると、バックスタックの状態が変わるので FragmentManager.OnBackStackChangedListener の onBackStackChanged() が呼ばれます。
次のコードを見てください。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "BackStackSample";
private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
final FragmentManager manager = getSupportFragmentManager();
final int count = manager.getBackStackEntryCount();
Log.d(TAG, "onBackStackChanged : " + count);
Log.d(TAG, "onBackStackChanged : " + manager.findFragmentById(R.id.container));
if (count > 0) {
Log.d(TAG, "onBackStackChanged : " + manager.getBackStackEntryAt(count - 1));
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final FragmentManager manager = getSupportFragmentManager();
manager.addOnBackStackChangedListener(backStackChangedListener);
findViewById(R.id.add_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
manager.beginTransaction()
.add(R.id.container, new FragmentA())
.addToBackStack(String.valueOf(System.currentTimeMillis()))
.commit();
}
});
findViewById(R.id.replace_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
manager.beginTransaction()
.replace(R.id.container, new FragmentB())
.addToBackStack(String.valueOf(System.currentTimeMillis()))
.commit();
}
});
}
@Override
protected void onDestroy() {
getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener);
super.onDestroy();
}
}
このコードを実行し、add ボタンをタップし、その次に replace ボタンをタップし、最後にバックキーをタップすると、次のようなログが出力されます。
25.0.1
D/BackStackSample: onBackStackChanged : 1
D/BackStackSample: onBackStackChanged : FragmentA{faac3a9 #0 id=0x7f0b0057}
D/BackStackSample: onBackStackChanged : BackStackEntry{108fd2e #0 1487898291683}
D/BackStackSample: onBackStackChanged : 2
D/BackStackSample: onBackStackChanged : FragmentB{d8c00cf #1 id=0x7f0b0057}
D/BackStackSample: onBackStackChanged : BackStackEntry{bf59a5c #1 1487898292435}
D/BackStackSample: onBackStackChanged : 1
D/BackStackSample: onBackStackChanged : FragmentA{faac3a9 #0 id=0x7f0b0057}
D/BackStackSample: onBackStackChanged : BackStackEntry{108fd2e #0 1487898291683}
25.1.0
D/BackStackSample: onBackStackChanged : 1
D/BackStackSample: onBackStackChanged : null
D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648}
D/BackStackSample: onBackStackChanged : 1
D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059}
D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648}
D/BackStackSample: onBackStackChanged : 2
D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059}
D/BackStackSample: onBackStackChanged : BackStackEntry{d8c00cf #1 1487898348305}
D/BackStackSample: onBackStackChanged : 2
D/BackStackSample: onBackStackChanged : FragmentB{bf59a5c #1 id=0x7f0b0059}
D/BackStackSample: onBackStackChanged : BackStackEntry{d8c00cf #1 1487898348305}
D/BackStackSample: onBackStackChanged : 1
D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059}
D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648}
なんてこったい。25.0.1 までは add() と replace() でそれぞれ1回しか onBackStackChanged() が呼ばれていなかったのに、25.1.0 からそれぞれ2回呼ばれるようになっているじゃあないですか。
しかも、add() または replace() 先の view id のついた Fragment がどれになっているかを見ると、2回呼ばれるうちの最初の方は Transaction が実行される前のようです。
一方、バックキーで pop するときは 25.0.1 と 25.1.0 で挙動は同じです。
バグ...のような気がしなくもないけれど 25.2.0 でも変わらず2回呼ばれます。
ActionBar のタイトルを foreground の Fragment に応じて変えるなどの処理を onBackStackChanged() 内でやっていたのですが、view id のついた Fragment が BackStack に入る方を指している状態で呼ばれると困るわけです。
ちなみに onBackStackChanged() をトリガーとする理由は、onAttachFragment() だとバックキーで pop されたときに呼ばれないからです。
妥当な対処方法としては
- 1. add() or replace() のときは onAttachFragment() で処理し、onBackStackChanged() に pop されたかどうかの判定を入れて pop された時だけ処理する
- 2. add() or replace() のときはその場処理し、onBackStackChanged() に pop されたかどうかの判定を入れて pop された時だけ処理する
ちなみに 1 でやるとこんな感じです。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "BackStackSample";
private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
private int backStackCount;
@Override
public void onBackStackChanged() {
final FragmentManager manager = getSupportFragmentManager();
final int count = manager.getBackStackEntryCount();
if (backStackCount > count) {
// pop された
final Fragment fragment = manager.findFragmentById(R.id.container);
if (fragment != null) {
onCurrentFragmentChanged(fragment);
}
}
backStackCount = count;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final FragmentManager manager = getSupportFragmentManager();
manager.addOnBackStackChangedListener(backStackChangedListener);
findViewById(R.id.add_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
manager.beginTransaction()
.add(R.id.container, new FragmentA())
.addToBackStack(String.valueOf(System.currentTimeMillis()))
.commit();
}
});
findViewById(R.id.replace_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
manager.beginTransaction()
.replace(R.id.container, new FragmentB())
.addToBackStack(String.valueOf(System.currentTimeMillis()))
.commit();
}
});
}
@Override
protected void onDestroy() {
getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener);
super.onDestroy();
}
@Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
onCurrentFragmentChanged(fragment);
}
private void onCurrentFragmentChanged(Fragment fragment) {
// ここで fragment に応じて ActionBar のタイトルを変えたりする
Log.d(TAG, "onCurrentFragmentChanged : " + fragment);
}
}
onBackStackChanged() だけで対処できていたのに、つらい...