【Android】FragmentPagerAdapterの中身を動的に変更する

注意

この記事はFragmentAdapterの使い方と言うよりは、FragmentPagerAdapter is クソみたいな話をします。

結論から言うと、FragmentPagerAdapter / FragmentStateAdapterの実装では内部のFragmentに対するreplaceやremoveは実質不可能です。

また、この記事に書いてあるアドホックな解決策は特定のパターンでエラーを吐いて死にます。

やるからにはFragmentPagerAdapterの親クラスであるPagerAdapterから実装し直さないといけません。

で、やりました。その辺の操作をやりたい人は以下の記事をご覧ください。

上記の記事は一応この記事を読んでいること前提として書いているので、まぁ、読むだけ読んでおいてください。

[追記ここまで]

前書き

今回もクロージャの話…ではなく、FragmentPagerAdapterの実装が結構アレだったって話をします。

PagerAdapterってなに?

Support PackageにViewPagerってViewがあります。スワイプでViewを切り替える、あれです。

PagerAdapterはそのViewPagerで表示する中身を生成するためのAdapterです。FragmentPagerAdapterを継承するとViewPagerで簡単にFragmentを扱うことが出来ます。

FragmentPagerAdapterを継承した具象クラスを作成する

ぶっちゃけ普通の使い方の説明をしたいわけじゃないので、ただ単に使うだけならこの辺を見たほうが参考になります。

とは言えコード例がないと先に進まないので、最低限の実装をしておきます。

public class MyFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> _fragments = new ArrayList<Fragment>();
    
    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }
    
    @Override
    public Fragment getItem(int position) {
        return _fragments.get(position);
    }
    
    @Override
    public int getCount() {
        return _fragments.size();
    }
    
    public void add(Fragment fragment) {
        _fragments.add(fragment);
    }
}

Activityも作っておきます。Fragmentの中身は今回どうでもいいのでそっちは割愛します。

public class FragmentPagerActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_fragment_pager);
        ViewPager vp = (ViewPager) findViewById(R.id.viewPager);
        
        MyFragmentPagerAdapter adapter = new new MyFragmentPagerAdapter(getSupportFragmentManager());
        adapter.add(new HogeFragment());
        
        vp.setAdapter(adapter);
    }
}

FragmentPagerAdapterを介して既にViewPagerに表示されているFragmentに変更を加える

ViewPagerに表示されているFragmentの中身だけを変えるメソッドが欲しくなったとしましょう。とりあえずreplaceと言うメソッドを追加します。

public class MyFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> _fragments = new ArrayList<Fragment>();
    
    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }
    
    @Override
    public Fragment getItem(int position) {
        return _fragments.get(position);
    }
    
    @Override
    public int getCount() {
        return _fragments.size();
    }
    
    public void add(Fragment fragment) {
        _fragments.add(fragment);
    }
    
    public void replace(int position, Fragment fragment) {
        _fragments.set(position, fragment);
    }
}

Activityから呼び出します。

public class FragmentPagerActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_fragment_pager);
        ViewPager vp = (ViewPager) findViewById(R.id.viewPager);
        
        MyFragmentPagerAdapter adapter = new new MyFragmentPagerAdapter(getSupportFragmentManager());
        adapter.add(new HogeFragment());
        
        vp.setAdapter(adapter);
        
        // replace
        adapter.replace(0, new PiyoFragment());
        adapter.notifyDataSetChanged();
    }
}

普通に考えたらPiyoFragmentが表示されそうです。が、されません。HogeFragmentのままです。

getItemPositionをオーバーライドする

PagerAdapterのnotifyDataSetChangedを呼ぶ場合はPagerAdapter#getItemPosition(Object object)をOverrideする必要があります。

まず第一のブチギレポイントとして、このメソッド名と返り値から「objectがある場所のpositionを返せばいいのかな???」と思わざるを得ないんですが、PagerAdapter#POSITION_NONEかPagerAdapter#POSITION_UNCHANGEDのどちらかを返す必要があります。まぁこれはドキュメントにも書いてあるのでおいおいもっとマシな名前なかったかよーで済むんですが、notifyDataSetChangedに関する言及が0なのはどう考えてもおかしいだろ。

まぁともかく実装しないと動かないので実装します。ってかNONEかUNCHANGEDかってそれ二択になってなくないか?CHANGEDはないのか?

public class MyFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> _fragments = new ArrayList<Fragment>();
    
    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }
    
    @Override
    public Fragment getItem(int position) {
        return _fragments.get(position);
    }
    
    @Override
    public int getCount() {
        return _fragments.size();
    }
    
    @Override
    public int getItemPosition(Object object) {
        // 面倒なので何も考えずにPOSITION_NONEを返す
        return POSITION_NONE;
    }
    
    public void add(Fragment fragment) {
        _fragments.add(fragment);
    }
    
    public void replace(int position, Fragment fragment) {
        _fragments.set(position, fragment);
    }
}

で、もう一回先ほどのActivityを動かしてみます。やっぱり変わりません。

FragmentPagerAdapterの実装を見てみる

どうやらFragmentPagerAdapterの中でFragmentがキャッシュされているようです。内部で何をやってるのか想像をめぐらせても時間の無駄なので、ソースをのぞいてみましょう。

@Override
public Object instantiateItem(View container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), position);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + position + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), position));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
    }

    return fragment;
}

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

どうもここが臭いです。

String name = makeFragmentName(container.getId(), position);
Fragment fragment = mFragmentManager.findFragmentByTag(name);

makeFragmentNameではcontaier(実体はViewPager)のidとpositionからFragmentのタグを作り出しています。mFragmentManagerはコンストラクタで渡したFragmentManagerです。

もう一度やろうとしていたことを思い出しましょう。replaceです。つまり、positionは変わりません。viewIdも当然変わりません。なので、FragmentManagerからそれをremoveしていないとfragment != nullになります。fragment != nullであれば、FragmentPagerAdapter#getItemが呼ばれません。これが原因ですね。

解決法

そう言うことであれば仕方ありません。replaceメソッドが呼ばれた時にFragmentManagerのFragmentもreplaceしてしまいしょう。

public class MyFragmentPagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> _fragments = new ArrayList<Fragment>();
    private FragmentManager _fm;
    private FragmentTransaction _ft;
    private int _pagerId;
    
    
    public MyFragmentPagerAdapter(int pagerId, FragmentManager fm) {
        super(fm);
        _fm = fm;
        _pagerId = pagerId;
    }
    
    @Override
    public Fragment getItem(int position) {
        return _fragments.get(position);
    }
    
    @Override
    public int getCount() {
        return _fragments.size();
    }
    
    @Override
    public int getItemPosition(Object object) {
        // 面倒なので何も考えずにPOSITION_NONEを返す
        return POSITION_NONE;
    }
    
    @Override
    public void notifyDataSetChanged() {
        if(_ft != null) {
            _ft.commitAllowingStateLoss()
            _ft = null;
        }
        
        super.notifyDataSetChanged();
    }
    
    public void add(Fragment fragment) {
        _fragments.add(fragment);
    }
    
    public void replace(int position, Fragment fragment) {
        String tag = makeFragmentName(_pagerId, position);
        
        if(_ft != null) _ft = _fm.beginTransaction();
        
        _ft.replace(_pagerId, fragment, tag);
        _fragments.set(position, fragment);
    }
    
    private static String makeFragmentName(int viewId, int index) {
        return "android:switcher:" + viewId + ":" + index;
    }
}

まとめ

removeをしたいとか、位置を変えたい場合もこれの応用でなんとかなります。

とは言え、大分無理矢理な実装です。FragmentManagerレベルでキャッシュされているとは言えgetItemPositionは全部強制的に再生成させてしまっていますし、FragmentTransactionをcommitさせるタイミングが違うせいで出てくる不具合もあるかもしれません。

寧ろPagerAdapterを継承して1からFragmentPagerAdapterを作ってしまったほうが簡単かもしれません。

参考