2017年2月24日金曜日

Support library 25.1.0 で OnBackStackChangedListener の挙動が変わっていた

追記: バグでした。 https://code.google.com/p/android/issues/detail?id=230353

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