2019年7月3日水曜日

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 編 : ViewPager + FragmentPagerAdapter での onResume() の挙動

ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動」の BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 編です。


FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。
  1. class SimpleFragment : Fragment() {  
  2.   
  3.     override fun onStart() {  
  4.         super.onStart()  
  5.         println("$position : onStart")  
  6.     }  
  7.   
  8.     override fun onResume() {  
  9.         super.onResume()  
  10.         println("$position : onResume")  
  11.     }  
  12.   
  13.     override fun onPause() {  
  14.         super.onPause()  
  15.         println("$position : onPause")  
  16.     }  
  17.   
  18.     override fun onStop() {  
  19.         super.onStop()  
  20.         println("$position : onStop")  
  21.     }  
  22. }  


1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)  
  8.     }  
  9. }  
この場合、ログは次のようになります。
  1. System.out: 0 : onStart  
  2. System.out: 1 : onStart  
  3. System.out: 0 : onResume  
つまり
  • 1. position 0 の Fragment の onStart()
  • 2. position 1 の Fragment の onStart()
  • 3. position 0 の Fragment の onResume()
という流れです。



2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合

onCreate() で ViewPager に adapter をセットした後 currentItem を 1 にしてみます。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)  
  8.         viewPager.currentItem = 1  
  9.     }  
  10. }  
この場合のログはこうなります。
  1. System.out: 1 : onStart  
  2. System.out: 0 : onStart  
  3. System.out: 2 : onStart  
  4. System.out: 1 : onResume  
つまり
  • 1. position 1 の Fragment の onStart()
  • 2. position 0 の Fragment の onStart()
  • 3. position 2 の Fragment の onStart()
  • 4. position 1 の Fragment の onResume()
という流れです。



3. レイアウトより後のタイミングで adapter をセットしている場合
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         Handler().postDelayed(  
  8.             {   
  9.                 viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)   
  10.             },  
  11.             1000  
  12.         )  
  13.     }  
  14. }  
onCreate() から1秒後に adapter をセットしてみます。この場合ログは次のようになります。
  1. System.out: 0 : onStart  
  2. System.out: 1 : onStart  
  3. System.out: 0 : onResume  
「1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合」と同じですね。
  • 1. position 0 の Fragment の onStart()
  • 2. position 1 の Fragment の onStart()
  • 3. position 0 の Fragment の onResume()
という流れです。



4. レイアウトより後のタイミングで adapter をセットし、currentItem を変更している場合

onCreate() から1秒後に adapter をセットし、currentItem を 1 にしてみます。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         Handler().postDelayed(  
  8.             {  
  9.                 viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)  
  10.                 viewPager.currentItem = 1  
  11.             },  
  12.             1000  
  13.         )  
  14.     }  
  15. }  
この場合ログは次のようになります。
  1. System.out: 0 : onStart  
  2. System.out: 1 : onStart  
  3. System.out: 0 : onResume  
  4.   
  5. System.out: 2 : onStart  
  6. System.out: 0 : onPause  
  7. System.out: 1 : onResume  
「2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合」と同じになりません!

まず「3. レイアウトより後のタイミングで adapter をセットしている場合」と同じことが起こります。

つまり
  • 1. position 0 の Fragment の onStart()
  • 2. position 1 の Fragment の onStart()
  • 3. position 0 の Fragment の onResume()
という流れが最初にあります。

その後
  • 4. position 2 の Fragment の onStart()
  • 5. position 0 の Fragment の onPause()
  • 6. position 1 の Fragment の onResume()
という流れになります。

adapter をセットしたタイミングで 1 〜 3 までの流れが起こり、currentItem を 1 にしたタイミングで 4 〜 6 の流れが起こります。

このように、レイアウトよりも後のタイミングで adapter をセットすると、その時点で position 0 の Fragment を PrimaryItem として onResume() まで呼ばれてしまいます。

そのため、レイアウトよりも後のタイミングで adapter をセットして currentItem を 0 以外に変更する場合、「Fragment がユーザーに表示されたタイミングで xx したい」処理のトリガーとして onResume() を使うには注意が必要です。

adapter がセットされている間の onResume() に反応してしまうと、その後 currentItem が変更されて実際にはユーザーにはほぼ見えない Fragment でも処理が走ってしまうため、adapter がセットされている間だけフラグを立てておいて onResume() で反応しないような工夫が必要になります。


androidx.fragment:fragment:1.1.0-alpha07 で userVisibleHint は deprecated になりました

ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動」の冒頭で軽く言及しましたが、androidx.fragment:fragment:1.1.0-alpha07 で FragmentPagerAdapter および FragmentStatePagerAdapter に変更が入っています。

1.1.0-alpha08 でも変更が入っており、以下の挙動は 1.1.0-rc01 で確認しています。


今まで FragmentPagerAdapter および FragmentStatePagerAdapter のコンストラクタでは FragmentManager だけを渡していましたが、1.1.0-alpha07 からはフラグ(int)も渡すように変わりました。

ここで指定できるフラグとして以下の2つが用意されています。
  • BEHAVIOR_SET_USER_VISIBLE_HINT
  • BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
今までの FragmentManager だけを渡すコンストラクタは deprecated になり、内部ではフラグとして BEHAVIOR_SET_USER_VISIBLE_HINT が指定されます。
  1. public abstract class FragmentPagerAdapter extends PagerAdapter {  
  2.   
  3.     ...  
  4.   
  5.     @Deprecated  
  6.     public FragmentPagerAdapter(@NonNull FragmentManager fm) {  
  7.         this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);  
  8.     }  
  9.   
  10.     ...  
  11. }  
つまり BEHAVIOR_SET_USER_VISIBLE_HINT を指定すると、1.1.0-alpha06 までと同じ挙動になるということです。

ただし、この BEHAVIOR_SET_USER_VISIBLE_HINT 自体も deprecated です。
  1. public abstract class FragmentPagerAdapter extends PagerAdapter {  
  2.   
  3.     ...  
  4.   
  5.     @Deprecated  
  6.     public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;  
  7.   
  8.     public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;  
  9.   
  10.     ...  
  11. }  
さらに Fragment の setUserVisibleHint(), getUserVisibleHint() も deprecated になりました。


では新しい BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT を指定するとどうなるのか、挙動を確認してみましょう。

FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。
  1. class SimpleFragment : Fragment() {  
  2.   
  3.     override fun setUserVisibleHint(isVisibleToUser: Boolean) {  
  4.         super.setUserVisibleHint(isVisibleToUser)  
  5.         println("$position : setUserVisibleHint : $isVisibleToUser")  
  6.     }  
  7.   
  8.     override fun onStart() {  
  9.         super.onStart()  
  10.         println("$position : onStart")  
  11.     }  
  12.   
  13.     override fun onResume() {  
  14.         super.onResume()  
  15.         println("$position : onResume")  
  16.     }  
  17.   
  18.     override fun onPause() {  
  19.         super.onPause()  
  20.         println("$position : onPause")  
  21.     }  
  22.   
  23.     override fun onStop() {  
  24.         super.onStop()  
  25.         println("$position : onStop")  
  26.     }  
  27. }  
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)  
  8.     }  
  9. }  
この場合、ログは次のようになります。
  1. System.out: 0 : onStart  
  2. System.out: 1 : onStart  
  3. System.out: 0 : onResume  
setUserVisibleHint() は呼ばれません。

position 0 と 1 の Fragment の onStart() が呼ばれ、その後 position 0 の Fragment だけ onResume() が呼ばれます。


この状態(position 0 が表示されいる状態)で右にスワイプして position 1 を表示すると、次のようなログが出ます。
  1. System.out: 2 : onStart  
  2. System.out: 0 : onPause  
  3. System.out: 1 : onResume  
position 2 の Fragment が生成されて onStart() が呼ばれ、その後 position 0 の Fragment の onPause() が呼ばれて、position 1 の Fragment の onResume() が呼ばれます。


この状態(position 1 が表示されいる状態)で左にスワイプして再度 position 0 を表示すると、次のようなログが出ます。
  1. System.out: 2 : onStop  
  2. System.out: 1 : onPause  
  3. System.out: 0 : onResume  
position 2 の Fragment が detach されるので onStop() が呼ばれています。
その後 position 1 の Fragment の onPause() が呼ばれて、position 0 の Fragment の onResume() が呼ばれます。


つまり、BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT を指定すると、PrimaryItem の Fragment だけが Lifecycle.State.RESUMED に達し、その他の Fragment は Lifecycle.State.STARTED で止まるという挙動になります。

onResume() が呼ばれるのは PrimaryItem の Fragment だけなので、「Fragment がユーザーに表示されたタイミングで xx したい」という場合に onResume() をトリガーとすることができるようになりました。



ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動

以下は androidx.fragment:fragment:1.0.0 での挙動です。1.1.0-alpha07 で別の挙動にするためのオプションが追加されています。



ViewPager + FragmentPagerAdapter の構成はよく使うと思います。

FragmentPagerAdapter では、ViewPager のページが切り替わる処理の中で、
以前の PrimaryItem の Fragment に対して setUserVisibleHint(false) を呼び、
新しい PrimaryItem の Fragment に対して setUserVisibleHint(true) を呼んでいます。


挙動を確認するために、FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。
  1. class SimpleFragment : Fragment() {  
  2.   
  3.     ...  
  4.   
  5.     override fun setUserVisibleHint(isVisibleToUser: Boolean) {  
  6.         println("$position : setUserVisibleHint : before super : to = $isVisibleToUser, current = $userVisibleHint")  
  7.         super.setUserVisibleHint(isVisibleToUser)  
  8.         println("$position : setUserVisibleHint : after  super : to = $isVisibleToUser, current = $userVisibleHint")  
  9.     }  
  10.   
  11.     override fun onStart() {  
  12.         super.onStart()  
  13.         println("$position : onStart : $userVisibleHint")  
  14.     }  
  15. }  


1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         viewPager.adapter = MyPager(supportFragmentManager)  
  8.     }  
  9. }  
この場合、ログは次のようになります。
  1. System.out: 0 : setUserVisibleHint : before super : to = false, current = true  
  2. System.out: 0 : setUserVisibleHint : after  super : to = false, current = false  
  3.   
  4. System.out: 1 : setUserVisibleHint : before super : to = false, current = true  
  5. System.out: 1 : setUserVisibleHint : after  super : to = false, current = false  
  6.   
  7. System.out: 0 : setUserVisibleHint : before super : to = true, current = false  
  8. System.out: 0 : setUserVisibleHint : after  super : to = true, current = true  
  9.   
  10. System.out: 0 : onStart : true  
  11. System.out: 1 : onStart : false  
まず position 0 と 1 の Fragment の setUserVisibleHint が false にセットされます。

super.setUserVisibleHint() が呼ばれる前の時点で取得した userVisibleHint が true ですね。 実は Fragment の mUserVisibleHint はデフォルトが true です。
  1. package androidx.fragment.app;  
  2.   
  3. ...  
  4.   
  5. public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner,  
  6.         ViewModelStoreOwner {  
  7.   
  8.     ...  
  9.   
  10.     // Hint provided by the app that this fragment is currently visible to the user.  
  11.     boolean mUserVisibleHint = true;  
  12.   
  13.     ...  
  14. }  

次に position 0 の Fragment の setUserVisibleHint が true にセットされます。

その後、Fragment の onStart() が呼ばれます。

つまり
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合

onCreate() で ViewPager に adapter をセットした後 currentItem を 1 にしてみます。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         viewPager.adapter = MyPager(supportFragmentManager)  
  8.         viewPager.currentItem = 1  
  9.     }  
  10. }  
この場合のログはこうなります。
  1. System.out: 1 : setUserVisibleHint : before super : to = false, current = true  
  2. System.out: 1 : setUserVisibleHint : after  super : to = false, current = false  
  3.   
  4. System.out: 0 : setUserVisibleHint : before super : to = false, current = true  
  5. System.out: 0 : setUserVisibleHint : after  super : to = false, current = false  
  6.   
  7. System.out: 2 : setUserVisibleHint : before super : to = false, current = true  
  8. System.out: 2 : setUserVisibleHint : after  super : to = false, current = false  
  9.   
  10. System.out: 1 : setUserVisibleHint : before super : to = true, current = false  
  11. System.out: 1 : setUserVisibleHint : after  super : to = true, current = true  
  12.   
  13. System.out: 1 : onStart : true  
  14. System.out: 0 : onStart : false  
  15. System.out: 2 : onStart : false  
まず position 1, 0, 2 の Fragment の setUserVisibleHint が false にセットされ、
次に position 1 の Fragment の setUserVisibleHint が true にセットされ、
その後、Fragment の onStart() が呼ばれます。

つまり
  • 1. position 1 の Fragment の setUserVisibleHint() : true → false
  • 2. position 0 の Fragment の setUserVisibleHint() : true → false
  • 3. position 2 の Fragment の setUserVisibleHint() : true → false
  • 4. position 1 の Fragment の setUserVisibleHint() : false → true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は true
  • 6. position 0 の Fragment の onStart() : setUserVisibleHint は false
  • 7. position 2 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



3. レイアウトより後のタイミングで adapter をセットしている場合
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         Handler().postDelayed(  
  8.             {   
  9.                 viewPager.adapter = MyPager(supportFragmentManager)   
  10.             },  
  11.             1000  
  12.         )  
  13.     }  
  14. }  
onCreate() から1秒後に adapter をセットしてみます。この場合ログは次のようになります。
  1. System.out: 0 : setUserVisibleHint : before super : to = false, current = true  
  2. System.out: 0 : setUserVisibleHint : after  super : to = false, current = false  
  3.   
  4. System.out: 1 : setUserVisibleHint : before super : to = false, current = true  
  5. System.out: 1 : setUserVisibleHint : after  super : to = false, current = false  
  6.   
  7. System.out: 0 : setUserVisibleHint : before super : to = true, current = false  
  8. System.out: 0 : setUserVisibleHint : after  super : to = true, current = true  
  9.   
  10. System.out: 0 : onStart : true  
  11. System.out: 1 : onStart : false  
「1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合」と同じですね。
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



4. レイアウトより後のタイミングで adapter をセットし、currentItem を変更している場合

onCreate() から1秒後に adapter をセットし、currentItem を 1 にしてみます。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         Handler().postDelayed(  
  8.             {  
  9.                 viewPager.adapter = MyPager(supportFragmentManager)  
  10.                 viewPager.currentItem = 1  
  11.             },  
  12.             1000  
  13.         )  
  14.     }  
  15. }  
この場合ログは次のようになります。
  1. System.out: 0 : setUserVisibleHint : before super : to = false, current = true  
  2. System.out: 0 : setUserVisibleHint : after  super : to = false, current = false  
  3.   
  4. System.out: 1 : setUserVisibleHint : before super : to = false, current = true  
  5. System.out: 1 : setUserVisibleHint : after  super : to = false, current = false  
  6.   
  7. System.out: 0 : setUserVisibleHint : before super : to = true, current = false  
  8. System.out: 0 : setUserVisibleHint : after  super : to = true, current = true  
  9.   
  10. System.out: 0 : onStart : true  
  11. System.out: 1 : onStart : false  
  12.   
  13. System.out: 2 : setUserVisibleHint : before super : to = false, current = true  
  14. System.out: 2 : setUserVisibleHint : after  super : to = false, current = false  
  15.   
  16. System.out: 0 : setUserVisibleHint : before super : to = false, current = true  
  17. System.out: 0 : setUserVisibleHint : after  super : to = false, current = false  
  18.   
  19. System.out: 1 : setUserVisibleHint : before super : to = true, current = false  
  20. System.out: 1 : setUserVisibleHint : after  super : to = true, current = true  
  21.   
  22. System.out: 2 : onStart : false  
「2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合」と同じになりません!

まず「3. レイアウトより後のタイミングで adapter をセットしている場合」と同じことが起こります。

つまり
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れが最初にあります。

その後
  • 6. position 2 の Fragment の setUserVisibleHint() : true → false
  • 7. position 0 の Fragment の setUserVisibleHint() : true → false
  • 8. position 1 の Fragment の setUserVisibleHint() : false → true
  • 9. position 2 の Fragment の onStart() : setUserVisibleHint は false
という流れになります。 adapter をセットしたタイミングで 1 〜 5 までの流れが起こり、currentItem を 1 にしたタイミングで 6 〜 9 の流れが起こります。

しかも position 1 が表示されているのに、onStart() 時の setUserVisibleHint 値を見ると position 0 が表示されているかのようです。


このように、レイアウトよりも後のタイミングで adapter をセットすると、その時点で position 0 の Fragment を PrimaryItem として onStart() まで呼ばれてしまいます。
そのため、レイアウトよりも後のタイミングで adapter をセットし、currentItem を 0 以外に変更する場合は setUserVisibleHint() や onStart() が呼ばれるタイミングに注意が必要です。