FragmentPagerAdapter を軽く眺めた

f:id:bps_tomoya:20190414131611j:plain

FragmentPagerAdapter を眺めてみたので、気付きポイントをメモする。 今回見たのは androidx.fragment:fragment:1.0.0 に同梱されているもの。

android.googlesource.com

冒頭のクラスのコメントで、ViewPager には ID が必須ですよ、という説明がある。

 * <p>When using FragmentPagerAdapter the host ViewPager must have a
 * valid ID set.</p>

ViewPager が ID を持っていない場合、ページ切り替わりが開始した時に呼び出される FragmentPagerAdapter#startUpdate(ViewGroup) の処理で怒られる。

@Override
public void startUpdate(@NonNull ViewGroup container) {
    if (container.getId() == View.NO_ID) {
        throw new IllegalStateException("ViewPager with adapter " + this
                + " requires a view id");
    }
}

何故 ID が必要になるのかというとFragment の生成時に一意な名前を付与するところで ID が使われているから。 順番が前後してしまうけれど FragmentPagerAdapter#makeFragmentName(int, long) が名前を作っている。

private static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
}

ViewPager がページを追加しようとするときに呼ばれるFramgnetPageAdapter#instantiateItem(ViewGroup, int) は position に合致する Fragment を表示するための操作を行い Fragment を返却する。

もし FragmentManager が既に持っている Fragment ならそれを取り出せばいいわけだけれど、その検索を実現するために FragmentPagerAdapter#makeFragmentName(int, long) が使われていることが分かる。

@SuppressWarnings("ReferenceEquality")
@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    final long itemId = getItemId(position);
    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }
    return fragment;

あまり見慣れなかったのが、

  • Fragment#setMenuVisibility(boolean)
  • Fragment#setUserVisibleHint(boolean)

この2つで、どちらも触った覚えがないのでドキュメントを確認した。

Fragment#setMenuVisibility(boolean)

Fragment のメニューを表示するかどうか。

Fragment#setUserVisibleHint(boolean)

Fragment がユーザに対して表示されているか、どうかを伝えるメソッド。 Fragment の表示状態に応じて何か処理をしたいときはこれを override するとよさそうだ。ただし Note にあるようにライフサイクルの順序保障はされていないので呼び出されるタイミングには注意したい。

余談だけれど Do we already have this fragment?のコメント、なんだかテンポ良くて好き。

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment)object).getView());
    mCurTransaction.detach((Fragment)object);
}
@SuppressWarnings("ReferenceEquality")
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

表示する Fragment が切り替わっていらなくなった Fragment や新たに表示された Fragment に対する処理。

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitNowAllowingStateLoss();
        mCurTransaction = null;
    }
}

表示されている View の変更が完了したときに呼び出される。 FragmentTransaction の操作を反映させて Fragment を表示して、mCurTransaction は次の FragmentTransaction を受けられるように null になる。

@Override
public Parcelable saveState() {
    return null;
}

@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}

アレッと思ったけれど今回見てるのは FragmentStatePagerAdapter ではなく FragmentPagerAdapter。 なのでここは何もしなくていいんですね。

public long getItemId(int position) {
    return position;
}

一意にアイテムを特定するための ID を返却、デフォルトの実装は単に position を返すだけ。 position とアイテムの関係性が変化したり都合が悪いのなら適宜 override してね、というやつ。

おわり。