これはYahoo! JAPAN 2018年新卒有志 Advent Calendar 2018の16日目の記事です。
前回は @44x1carbon さんによる「日本語プログラミングのすヽめ」でした。
背景
先日、業務で「メイン画面のタブの左に一つカテゴリーを増やしたいが、サーバーの負荷を考えてほしい」と言われました。
ViewPagerでFragmentPagerAdapterやFragmentStatePagerAdapterを利用している場合、デフォルトでは今表示しているFragmentの左右1つずつのFragmentも読み込まれるようになっています。
ユーザーがスワイプしたときに滑らかに遷移するためには必要なのですが、多くのデータを取得する必要があった場合、
- 表示していないのに通信が発生する。(無駄にパケットを食う)
- 必要以上にサーバーに負荷がかかる。
といった問題があります。
これにどう対処したかについて書いていきます。
ViewPager#setOffsetScreenPageLimit
ViewPagerにはキャッシュする画面数を変更するsetOffSecreenPageLimit
というメソッドが存在します。
例えば、viewPager.offscreenPageLimit = 2
のようにしておけば、表示している左右2つずつのFragmentが読み込まれます。(デフォルトは1)
「じゃあviewPager.offscreenPageLimit = 0
すればいいじゃん。」と思うのですが、setOffScreenPageLimit
の実装を見ると、
public void setOffscreenPageLimit(int limit) {
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
mOffscreenPageLimit = limit;
populate();
}
}
のようになっており、DEFAULT_OFFSCREEN_PAGES(つまり1)未満の値をセットできません。
Fragment#setUserVisibleHint
FragmentにはsetUserVisibleHint
というほぼViewPagerのためだけのメソッドが存在しています。
ViewPagerでの表示Fragmentが変更されるたびに呼び出され、現在そのFragmnetが表示されているかどうかがわたってきます。
そこで、onActivityCreated
などに書いているデータ取得処理をここで行ってしまえば、「最初に表示されたときのみデータを取得する」という処理を書くことができます。
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
val lastValue = userVisibleHint
super.setUserVisibleHint(isVisibleToUser)
if (!lastValue && isVisibleToUser) {
// TODO: データ取得処理
}
}
しかし、setUserVisibleHint
はonViewCreated
やonActivityCreated
より後に呼ばれる可能性があります。
AACのViewModelをonActivityCreated
などで生成し、そのViewModelからデータ取得を行おうとするとUninitializedPropertyAccessException
が発生する可能性があります。
class PageFragment : Fragment() {
private lateinit var viewModel: FeaturePageViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val viewModel = ViewModelProviders.of(
parentFragment!!,
PageViewModel.Factory(activity!!.application)
).get(PageViewModel::class.java)
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
val lastValue = userVisibleHint
super.setUserVisibleHint(isVisibleToUser)
if (!lastValue && isVisibleToUser) {
// データ取得処理
viewModel.load() //UninitializedPropertyAccessException発生
}
}
}
そこで、AACのLifecycleを利用します。
getLifecycle.getCurrentState()
で現在のStateを確認できるため、以下のようにします。
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
val lastValue = userVisibleHint
super.setUserVisibleHint(isVisibleToUser)
if (!lastValue && isVisibleToUser) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// データ取得処理
viewModel.load()
}
}
}
これで画面が表示されたときのみデータを取得するという処理ができました。
最後に
今回はデータ取得のタイミングをずらすことで対応しましたが、ユーザー体験的にはViewPagerは先読みされていたほうが快適です。
やむを得ない事情がある場合のみ対応するようにしましょう。
また、今回は書きませんでしたが、上の例ではPageFragmentが持つPageViewModelのスコープをthis
ではなくparentFragment
やactivity
にすることで、ViewPagerのキャッシュからPageFragmentが外れても取得したデータが保持できるようになっています。
この場合、viewModel.load()
する度にViewModelに追加していくような処理になっていると、スワイプを繰り返した場合に複数回データ取得処理が走るため、それを防ぐ必要があります。
viewModel.load()
で確認するなどの処理を行うようにしましょう。