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() が呼ばれます。

次のコードを見てください。
  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     private static final String TAG = "BackStackSample";  
  4.   
  5.     private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {  
  6.   
  7.         @Override  
  8.         public void onBackStackChanged() {  
  9.             final FragmentManager manager = getSupportFragmentManager();  
  10.             final int count = manager.getBackStackEntryCount();  
  11.             Log.d(TAG, "onBackStackChanged : " + count);  
  12.             Log.d(TAG, "onBackStackChanged : " + manager.findFragmentById(R.id.container));  
  13.             if (count > 0) {  
  14.                 Log.d(TAG, "onBackStackChanged : " + manager.getBackStackEntryAt(count - 1));  
  15.             }  
  16.         }  
  17.     };  
  18.   
  19.     @Override  
  20.     protected void onCreate(Bundle savedInstanceState) {  
  21.         super.onCreate(savedInstanceState);  
  22.         setContentView(R.layout.activity_main);  
  23.   
  24.         final FragmentManager manager = getSupportFragmentManager();  
  25.   
  26.         manager.addOnBackStackChangedListener(backStackChangedListener);  
  27.   
  28.         findViewById(R.id.add_button).setOnClickListener(new View.OnClickListener() {  
  29.             @Override  
  30.             public void onClick(View v) {  
  31.                 manager.beginTransaction()  
  32.                         .add(R.id.container, new FragmentA())  
  33.                         .addToBackStack(String.valueOf(System.currentTimeMillis()))  
  34.                         .commit();  
  35.             }  
  36.         });  
  37.   
  38.         findViewById(R.id.replace_button).setOnClickListener(new View.OnClickListener() {  
  39.             @Override  
  40.             public void onClick(View v) {  
  41.                 manager.beginTransaction()  
  42.                         .replace(R.id.container, new FragmentB())  
  43.                         .addToBackStack(String.valueOf(System.currentTimeMillis()))  
  44.                         .commit();  
  45.             }  
  46.         });  
  47.     }  
  48.   
  49.     @Override  
  50.     protected void onDestroy() {  
  51.         getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener);  
  52.         super.onDestroy();  
  53.     }  
  54. }  



このコードを実行し、add ボタンをタップし、その次に replace ボタンをタップし、最後にバックキーをタップすると、次のようなログが出力されます。

25.0.1
  1. D/BackStackSample: onBackStackChanged : 1  
  2. D/BackStackSample: onBackStackChanged : FragmentA{faac3a9 #0 id=0x7f0b0057}  
  3. D/BackStackSample: onBackStackChanged : BackStackEntry{108fd2e #0 1487898291683}  
  4.   
  5. D/BackStackSample: onBackStackChanged : 2  
  6. D/BackStackSample: onBackStackChanged : FragmentB{d8c00cf #1 id=0x7f0b0057}  
  7. D/BackStackSample: onBackStackChanged : BackStackEntry{bf59a5c #1 1487898292435}  
  8.   
  9. D/BackStackSample: onBackStackChanged : 1  
  10. D/BackStackSample: onBackStackChanged : FragmentA{faac3a9 #0 id=0x7f0b0057}  
  11. D/BackStackSample: onBackStackChanged : BackStackEntry{108fd2e #0 1487898291683}  


25.1.0
  1. D/BackStackSample: onBackStackChanged : 1  
  2. D/BackStackSample: onBackStackChanged : null  
  3. D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648}  
  4. D/BackStackSample: onBackStackChanged : 1  
  5. D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059}  
  6. D/BackStackSample: onBackStackChanged : BackStackEntry{faac3a9 #0 1487898347648}  
  7.   
  8. D/BackStackSample: onBackStackChanged : 2  
  9. D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059}  
  10. D/BackStackSample: onBackStackChanged : BackStackEntry{d8c00cf #1 1487898348305}  
  11. D/BackStackSample: onBackStackChanged : 2  
  12. D/BackStackSample: onBackStackChanged : FragmentB{bf59a5c #1 id=0x7f0b0059}  
  13. D/BackStackSample: onBackStackChanged : BackStackEntry{d8c00cf #1 1487898348305}  
  14.   
  15. D/BackStackSample: onBackStackChanged : 1  
  16. D/BackStackSample: onBackStackChanged : FragmentA{108fd2e #0 id=0x7f0b0059}  
  17. 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 でやるとこんな感じです。
  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     private static final String TAG = "BackStackSample";  
  4.   
  5.     private final FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {  
  6.   
  7.         private int backStackCount;  
  8.   
  9.         @Override  
  10.         public void onBackStackChanged() {  
  11.             final FragmentManager manager = getSupportFragmentManager();  
  12.             final int count = manager.getBackStackEntryCount();  
  13.             if (backStackCount > count) {  
  14.                 // pop された  
  15.                 final Fragment fragment = manager.findFragmentById(R.id.container);  
  16.                 if (fragment != null) {  
  17.                     onCurrentFragmentChanged(fragment);  
  18.                 }  
  19.             }  
  20.             backStackCount = count;  
  21.         }  
  22.     };  
  23.   
  24.     @Override  
  25.     protected void onCreate(Bundle savedInstanceState) {  
  26.         super.onCreate(savedInstanceState);  
  27.         setContentView(R.layout.activity_main);  
  28.   
  29.         final FragmentManager manager = getSupportFragmentManager();  
  30.   
  31.         manager.addOnBackStackChangedListener(backStackChangedListener);  
  32.   
  33.         findViewById(R.id.add_button).setOnClickListener(new View.OnClickListener() {  
  34.             @Override  
  35.             public void onClick(View v) {  
  36.                 manager.beginTransaction()  
  37.                         .add(R.id.container, new FragmentA())  
  38.                         .addToBackStack(String.valueOf(System.currentTimeMillis()))  
  39.                         .commit();  
  40.             }  
  41.         });  
  42.   
  43.         findViewById(R.id.replace_button).setOnClickListener(new View.OnClickListener() {  
  44.             @Override  
  45.             public void onClick(View v) {  
  46.                 manager.beginTransaction()  
  47.                         .replace(R.id.container, new FragmentB())  
  48.                         .addToBackStack(String.valueOf(System.currentTimeMillis()))  
  49.                         .commit();  
  50.             }  
  51.         });  
  52.     }  
  53.   
  54.     @Override  
  55.     protected void onDestroy() {  
  56.         getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener);  
  57.         super.onDestroy();  
  58.     }  
  59.   
  60.     @Override  
  61.     public void onAttachFragment(Fragment fragment) {  
  62.         super.onAttachFragment(fragment);  
  63.         onCurrentFragmentChanged(fragment);  
  64.     }  
  65.   
  66.     private void onCurrentFragmentChanged(Fragment fragment) {  
  67.         // ここで fragment に応じて ActionBar のタイトルを変えたりする  
  68.         Log.d(TAG, "onCurrentFragmentChanged : " + fragment);  
  69.     }  
  70. }  


onBackStackChanged() だけで対処できていたのに、つらい...