search
LoginSignup
12

More than 3 years have passed since last update.

posted at

updated at

ViewPagerの先読みデータ取得をやめる

これは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: データ取得処理
    }
}

しかし、setUserVisibleHintonViewCreatedonActivityCreatedより後に呼ばれる可能性があります。
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ではなくparentFragmentactivityにすることで、ViewPagerのキャッシュからPageFragmentが外れても取得したデータが保持できるようになっています。
この場合、viewModel.load()する度にViewModelに追加していくような処理になっていると、スワイプを繰り返した場合に複数回データ取得処理が走るため、それを防ぐ必要があります。
viewModel.load()で確認するなどの処理を行うようにしましょう。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
12