AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

FragmentでViewPagerを使うときにFragmentPagerAdapterに渡すFragmentManagerの注意点

嵌ったのでメモ。

BottomNavigationBar のタブの Fragment 内で ViewPager を使っていて、そのタブから別タブに切り替えて再びそのタブに戻ってきたとき、FragmentPagerAdapter#getItem() が呼ばれず、 ViewPager 内が再読込みされない現象が起きた。

調査の結果、 FragmentPagerAdapterインスタンスを作るときに渡す FragmentManagerFragmentActivity#getSupportFragmentManager() だと起きる。

そもそも FragmentPagerAdapter はメモリ上に全てViewPagerの子Fragment達を保持する。そのためタブを切り替えて戻ってきたときには以前の Fragment が使い回される。

FragmentPagerAdapter  |  Android Developers

実際に FragmentPagerAdapter#getItem() が呼ばれる付近のコードを見てみると確かに Fragment が保存されている場合は FragmentPagerAdapter#getItem() が呼ばれない。

public abstract class FragmentPagerAdapter extends PagerAdapter {
    ...
    
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        long itemId = this.getItemId(position);
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            this.mCurTransaction.attach(fragment);
        } else {
            fragment = this.getItem(position);
            this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }

        if (fragment != this.mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }
    ...
}

BottomNavigationView のタブ切り替えでは Activity は生きたままなのでその FragmentManagerViewPager内の子Fragment達を保持したままになり、 FragmentPagerAdapter#getItem() が呼ばれない。


↓の回答を見ると Fragment#getChildFragmentManager()FragmentPagerAdapterに渡すといいとある。 stackoverflow.com

この場合、保持される ViewPager 内の子Fragment達はその親FragmentFragmentManagerで管理される。タブを切り替えたときには通常、OnNavigationItemSelectedListener#onNavigationItemSelected()で親Fragmentの新しいインスタンスを作るので、FragmentManagerもリセットされ、 FragmentPagerAdapter#getItem() が呼ばれて再読込みが始まるようになる。

ViewPager のスワイプが起きたときは 親 Fragment が生きている限り、 FragmentManager 内で子 Fragment 達が管理されるので、通常通り効率的にスワイプできるようになる。

ちなみにこの問題は子Fragmentをメモリ上に保持しないFragmentStatePagerAdapterでは発生しない。これはページ数が多いなどメモリ保持するには重い場合に使う。

FragmentStatePagerAdapter  |  Android Developers

↑StackOverflowの回答の中で一番+が多い案は FragmentStatePagerAdapterを使う案である。この場合はメモリ上に子Fragmentを保持しないのでスワイプ時の挙動が変わる。どちらが良いかはページ数などの状況で決めるといいと思う。