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() だけで対処できていたのに、つらい...