web-dev-qa-db-ja.com

FragmentPagerAdapterを使用するときに既存のフラグメントを取得する方法

タブの管理とActivityを接続するすべての詳細を実装するヘルパークラスとしてFragmentPagerAdapterを使用しているViewPagerを介してフラグメントを相互に通信させるのに問題があります関連TabHost。 AndroidサンプルプロジェクトSupport4Demosで提供されるのと同じように、FragmentPagerAdapterを実装しました。

主な質問は、IDもタグも持っていないときに、どのようにしてFragmentManagerから特定のフラグメントを取得できますか? FragmentPagerAdapterはフラグメントを作成し、IDとタグを自動生成しています。

96
Ismar Slomic

問題の概要

注:この回答では、FragmentPagerAdapterとそのソースコードを参照します。ただし、一般的な解決策はFragmentStatePagerAdapter.にも適用する必要があります。

これを読んでいる場合は、おそらくFragmentPagerAdapter/FragmentStatePagerAdapterFragmentsViewPagerを作成することを意図していることを既にご存知でしょうが、アクティビティの再作成時に(デバイスの回転またはシステムがメモリを回復するためにアプリを殺すかどうかにかかわらず)これらのFragmentsは作成されません繰り返しますが、代わりに FragmentManagerから取得したインスタンス 。ここで、Activityを操作するには、これらのFragmentsへの参照を取得する必要があると言います。 id内部で設定 であるため、これらの作成されたtagにはFragmentsまたはFragmentPagerAdapterがありません。だから問題は、その情報なしでそれらへの参照を取得する方法です...

現在のソリューションの問題:内部コードに依存

私がこれまで見た多くの解決策と同様の質問は、FragmentManager.findFragmentByTag()を呼び出して 内部で作成されたタグ:"Android:switcher:" + viewId + ":" + idを模倣することにより、既存のFragmentへの参照を取得することに依存します 。これに関する問題は、内部ソースコードに依存していることです。これは、誰もが知っているように、永遠に同じままであるとは限りません。 GoogleのAndroidエンジニアは、既存のtagへの参照を見つけることができなくなるコードを壊すFragments構造を変更することを簡単に決定できました。

内部tagに依存しない代替ソリューション

以下は、Fragmentsに設定された内部FragmentPagerAdapterに依存しないtagsによって返されるFragmentsへの参照を取得する方法の簡単な例です。重要なのは、 instantiateItem() をオーバーライドして、getItem() instead の参照を保存することです。

public class SomeActivity extends Activity {
    private FragmentA m1stFragment;
    private FragmentB m2ndFragment;

    // other code in your Activity...

    private class CustomPagerAdapter extends FragmentPagerAdapter {
        // other code in your custom FragmentPagerAdapter...

        public CustomPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            // Do NOT try to save references to the Fragments in getItem(),
            // because getItem() is not always called. If the Fragment
            // was already created then it will be retrieved from the FragmentManger
            // and not here (i.e. getItem() won't be called again).
            switch (position) {
                case 0:
                    return new FragmentA();
                case 1:
                    return new FragmentB();
                default:
                    // This should never happen. Always account for each position above
                    return null;
            }
        }

        // Here we can finally safely save a reference to the created
        // Fragment, no matter where it came from (either getItem() or
        // FragmentManger). Simply save the returned Fragment from
        // super.instantiateItem() into an appropriate reference depending
        // on the ViewPager position.
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
            // save the appropriate reference depending on position
            switch (position) {
                case 0:
                    m1stFragment = (FragmentA) createdFragment;
                    break;
                case 1:
                    m2ndFragment = (FragmentB) createdFragment;
                    break;
            }
            return createdFragment;
        }
    }

    public void someMethod() {
        // do work on the referenced Fragments, but first check if they
        // even exist yet, otherwise you'll get an NPE.

        if (m1stFragment != null) {
            // m1stFragment.doWork();
        }

        if (m2ndFragment != null) {
            // m2ndFragment.doSomeWorkToo();
        }
    }
}

またはクラスメンバー変数/ tagsへの参照の代わりにFragmentsを使用したい場合は、tagsによって設定されたFragmentPagerAdapterを同じ方法で取得することもできます。注:これはFragmentStatePagerAdapterには適用されませんtagsを作成するときにFragmentsを設定しません。

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
    // get the tags set by FragmentPagerAdapter
    switch (position) {
        case 0:
            String firstTag = createdFragment.getTag();
            break;
        case 1:
            String secondTag = createdFragment.getTag();
            break;
    }
    // ... save the tags somewhere so you can reference them later
    return createdFragment;
}

このメソッドは、tagによって設定される内部FragmentPagerAdapterの模倣に依存せず、代わりにそれらを取得するために適切なAPIを使用することに注意してください。このようにすると、tagの将来のバージョンでSupportLibraryが変更されても、安全です。


忘れないでくださいActivityの設計に応じて、作業しようとしているFragmentsがまだ存在する場合と存在しない場合があるため、使用する前にnullチェックを実行して、参照。

また、代わりにFragmentStatePagerAdapterを使用している場合は、Fragmentsへのハード参照を保持したくありません。代わりに、Fragment参照を標準変数ではなくWeakReference変数に保存します。このような:

WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
    // reference hasn't been cleared yet; do work...
}
181
Tony Chan

私は次の投稿に基づいて私の質問に対する答えを見つけました: fragmentpageradapterでフラグメントを再利用する

私が学んだことはほとんどありません:

  1. FragmentPagerAdapter内のgetItem(int position)は、このメソッドが実際に行うことのやや誤解を招く名前です。既存のフラグメントを返すのではなく、新しいフラグメントを作成します。その意味で、メソッドはAndroid SDKのcreateItem(int position)のような名前に変更する必要があります。したがって、このメソッドはフラグメントの取得には役立ちません。
  2. 投稿の説明に基づいて 古いフラグメントへのFragmentPagerAdapterholds参照をサポート フラグメントの作成はFragmentPagerAdapterに任せる必要があります。つまり、フラグメントまたはそのタグへの参照はありません。ただし、フラグメントタグがある場合は、findFragmentByTag()を呼び出すことで、FragmentManagerからそのタグへの参照を簡単に取得できます。特定のページ位置でフラグメントのタグを見つける方法が必要です。

ソリューション

次のヘルパーメソッドをクラスに追加して、フラグメントタグを取得し、findFragmentByTag()メソッドに送信します。

private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
     return "Android:switcher:" + viewPagerId + ":" + fragmentPosition;
}

注意!これは、FragmentPagerAdapterが新しいフラグメントを作成するときに使用するのと同じ方法です。このリンクを参照してください http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/Android/support/v2/app/FragmentPagerAdapter.Java#104

80
Ismar Slomic

instantiateItemをオーバーライドする必要も、内部makeFragmentNameメソッドでフラグメントタグを作成する互換性に依存する必要もありません。 instantiateItempublicメソッドなので、呼び出すことができます(実際にはshould)アクティビティのonCreateメソッドで、フラグメントのインスタンスへの参照を取得し、必要に応じてローカル変数に保存します。 instantiateItemjavadoc で説明されているように、一連のstartUpdate呼び出しをfinishUpdateおよびPagerAdapterメソッドで囲むことを忘れないでください。

PagerAdapterメソッドstartUpdate(ViewGroup)の呼び出しは、ViewPagerのコンテンツが変更されようとしていることを示します。 instantiateItem(ViewGroup、int)またはdestroyItem(ViewGroup、int、Object)への1つ以上の呼び出しが続き、finishUpdate(ViewGroup)への呼び出しによって更新の終了が通知されます。

したがって、たとえば、これはonCreateメソッドでタブフラグメントへの参照を保存する方法です。

public class MyActivity extends AppCompatActivity {

    Fragment0 tab0; Fragment1 tab1;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.myLayout);
        ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager);
        MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
        viewPager.setAdapter(adapter);
        ((TabLayout) findViewById(R.id.tabs)).setupWithViewPager(viewPager);

        adapter.startUpdate(viewPager);
        tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0);
        tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1);
        adapter.finishUpdate(viewPager);
    }

    class MyPagerAdapter extends FragmentPagerAdapter {

        public MyPagerAdapter(FragmentManager manager) {super(manager);}

        @Override public int getCount() {return 2;}

        @Override public Fragment getItem(int position) {
            if (position == 0) return new Fragment0();
            if (position == 1) return new Fragment1();
            return null;  // or throw some exception
        }

        @Override public CharSequence getPageTitle(int position) {
            if (position == 0) return getString(R.string.tab0);
            if (position == 1) return getString(R.string.tab1);
            return null;  // or throw some exception
        }
    }
}

instantiateItemは、最初にFragmentManagerから既存のフラグメントインスタンスへの参照を取得しようとします。まだ存在しない場合にのみ、アダプタからgetItemメソッドを使用して新しいものを作成し、将来の使用のためにFragmentManagerに「保存」します。

追加情報:
instantiateItemメソッドでstartUpdate/finishUpdateに囲まれたonCreateメソッドを呼び出さないと、フラグメントインスタンスがcommittedto FragmentManager:アクティビティがフォアグラウンドになると、instantiateItemが自動的に呼び出されてフラグメントを取得しますが、startUpdate/finishUpdatemaynot(実装の詳細に応じて)および基本的には、FragmentTransactionを開始/コミットします。
これにより、作成されたフラグメントインスタンスへの参照が非常に迅速に失われ(画面を回転させる場合など)、必要以上に頻繁に再作成されます。フラグメントがどの程度「重い」かによって、パフォーマンスに無視できない影響が生じる場合があります。
しかし、より重要なのは、そのような場合、ローカル変数に保存されたフラグメントのインスタンスが古くなることです。Androidプラットフォームは、FragmentManagerから同じインスタンスを取得できなかったため、新しい変数を使用しますが、変数は引き続き古い変数を参照します。

12
morgwai

私がやった方法は、WeakReferencesのHashtableを次のように定義することです。

protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;

次に、次のようにgetItem()メソッドを作成しました。

@Override
public Fragment getItem(int position) {

    Fragment fragment;
    switch(position) {
    case 0:
        fragment = new MyFirstFragmentClass();
        break;

    default:
        fragment = new MyOtherFragmentClass();
        break;
    }

    fragmentReferences.put(position, new WeakReference<Fragment>(fragment));

    return fragment;
}

次に、メソッドを記述できます。

public Fragment getFragment(int fragmentId) {
    WeakReference<Fragment> ref = fragmentReferences.get(fragmentId);
    return ref == null ? null : ref.get();
}

これはうまくいくようで、私はそれが

"Android:switcher:" + viewId + ":" + position

fragmentPagerAdapterの実装方法に依存しないため、トリック。もちろん、フラグメントがFragmentPagerAdapterによってリリースされている場合、またはまだ作成されていない場合、getFragmentはnullを返します。

誰かがこのアプローチで何か間違ったことを見つけた場合、コメントは大歓迎です。

11
personne3000

現在のフラグメントへの参照を取得するために機能しているこのメソッドを作成しました。

public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) {
    try {
        Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class);
        Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager");
        f.setAccessible(true);
        FragmentManager fm = (FragmentManager) f.get(adapter);
        m.setAccessible(true);
        String tag = null;
        tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem());
        return fm.findFragmentByTag(tag);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } 
    return null;
}
10
Pepijn

フラグメントへのハンドルを取得する主な障害は、getItem()に依存できないことです。方向の変更後、フラグメントへの参照はnullになり、getItem()は再度呼び出されません。

タグを取得するためにFragmentPagerAdapterの実装に依存しないアプローチを次に示します。 getItem()から作成されたフラグメントまたはフラグメントマネージャから見つかったフラグメントを返すinstantiateItem()をオーバーライドします。

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Object value =  super.instantiateItem(container, position);

    if (position == 0) {
        someFragment = (SomeFragment) value;
    } else if (position == 1) {
        anotherFragment = (AnotherFragment) value;
    }

    return value;
}
2
Adam

@ personne3000が提案する解決策は素晴らしいですが、1つの問題があります。アクティビティがバックグラウンドになり、システムによって(空きメモリを確保するために)強制終了され、復元されると、fragmentReferencesが空になり、 getItemが呼び出されないためです。

以下のクラスは、このような状況を処理します。

public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter {

    public static final String FRAGMENT_SAVE_PREFIX = "holder";
    private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent's field is private and has no getters.

    public AbstractHolderFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
        fragmentManager = fm;
    }

    private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>();

    protected void holdFragment(F fragment) {
        holdFragment(holder.size(), fragment);
    }

    protected void holdFragment(int position, F fragment) {
        if (fragment != null)
            holder.put(position, new WeakReference<F>(fragment));
    }

    public F getHoldedItem(int position) {
        WeakReference<F> ref = holder.get(position);
        return ref == null ? null : ref.get();
    }

    public int getHolderCount() {
        return holder.size();
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google's FragmentStatePagerAdapter implementation
        super.restoreState(state, loader);
        Bundle bundle = (Bundle) state;
        for (String key : bundle.keySet()) {
            if (key.startsWith(FRAGMENT_SAVE_PREFIX)) {
                int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length()));
                Fragment f = fragmentManager.getFragment(bundle, key);
                holdFragment(index, (F) f);
            }
        }
    }

    @Override
    public Parcelable saveState() {
        Bundle state = (Bundle) super.saveState();
        if (state == null)
            state = new Bundle();

        for (int i = 0; i < holder.size(); i++) {
            int id = holder.keyAt(i);
            final F f = getHoldedItem(i);
            String key = FRAGMENT_SAVE_PREFIX + id;
            fragmentManager.putFragment(state, key, f);
        }
        return state;
    }
}
2
netimen

子フラグメントまたはプライマリ(現在表示されている)フラグメントにアクセスする必要がある場合は、常にこの基本クラスを使用します。上書きされたメソッドは、新しいフラグメントインスタンスが作成されたときとインスタンスがFragmentManagerから受信されたときの両方で呼び出されるため、実装の詳細に依存せず、ライフサイクルの変更に注意します。

public abstract class FragmentPagerAdapterExt extends FragmentPagerAdapter {

    private final ArrayList<Fragment> mFragments;
    private Fragment mPrimaryFragment;

    public FragmentPagerAdapterExt(FragmentManager fm) {
        super(fm);
        mFragments = new ArrayList<>(getCount());
    }

    @Override public Object instantiateItem(ViewGroup container, int position) {
        Object object = super.instantiateItem(container, position);
        mFragments.add((Fragment) object);
        return object;
    }

    @Override public void destroyItem(ViewGroup container, int position, Object object) {
        mFragments.remove(object);
        super.destroyItem(container, position, object);
    }

    @Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);
        mPrimaryFragment = (Fragment) object;
    }

    /** Returns currently visible (primary) fragment */
    public Fragment getPrimaryFragment() {
        return mPrimaryFragment;
    }

    /** Returned list can contain null-values for not created fragments */
    public List<Fragment> getFragments() {
        return Collections.unmodifiableList(mFragments);
    }
}
2

タグの代わりにIDを使用して、この問題を解決できました。 (私はカスタムフラグメントを使用するFragmentStatePagerAdapterを使用しています。このフラグメントでは、onAttachメソッドをオーバーライドし、IDをどこかに保存します。

@Override
public void onAttach(Context context){
    super.onAttach(context);
    MainActivity.fragId = getId();
}

そして、アクティビティ内でフラグメントに簡単にアクセスします。

Fragment f = getSupportFragmentManager.findFragmentById(fragId);
1
user2740905

FragmentPagerAdapterからフラグメントを返す方法については、 この投稿 をご覧ください。フラグメントのインデックスを知っていることに依存していますが、これはgetItem()で設定されます(インスタンス化時のみ)

0
user1350683

このクラスは、内部タグに依存せずにトリックを実行します。警告:フラグメントには、getItemメソッドではなくgetFragmentメソッドを使用してアクセスする必要があります。

public class ViewPagerAdapter extends FragmentPagerAdapter {

    private final Map<Integer, Reference<Fragment>> fragments = new HashMap<>();
    private final List<Callable0<Fragment>> initializers = new ArrayList<>();
    private final List<String> titles = new ArrayList<>();

    public ViewPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    void addFragment(Callable0<Fragment> initializer, String title) {
        initializers.add(initializer);
        titles.add(title);
    }

    public Optional<Fragment> getFragment(int position) {
        return Optional.ofNullable(fragments.get(position).get());
    }

    @Override
    public Fragment getItem(int position) {
        Fragment fragment =  initializers.get(position).execute();
        return fragment;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        fragments.put(position, new WeakReference<>(fragment));
        return fragment;
    }

    @Override
    public int getCount() {
        return initializers.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return titles.get(position);
    }
}
0
Alex Miragall

これが最良のアプローチであるかどうかはわかりませんが、他に何もうまくいきませんでした。 getActiveFragmentを含む他のすべてのオプションがnullを返すか、アプリがクラッシュしました。

画面の回転でフラグメントが添付されていることに気付いたので、フラグメントを使用してフラグメントをアクティビティに送り返しました。

フラグメント内:

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    try {
        mListener = (OnListInteractionListener) activity;
        mListener.setListFrag(this);
    } catch (ClassCastException e) {
        throw new ClassCastException(activity.toString()
                + " must implement OnFragmentInteractionListener");
    }
}

次に、アクティビティで:

@Override
public void setListFrag(MyListFragment lf) {
    if (mListFragment == null) {
        mListFragment = lf;
    }
}

最後に、アクティビティonCreate()で:

if (savedInstanceState != null) {
    if (mListFragment != null)
        mListFragment.setListItems(items);
}

このアプローチでは、新しいフラグメントを作成せずに、実際の表示可能なフラグメントをアクティビティに添付します。

0
TacoEater

私はJava/Androidの比較的初心者なので、私の方法がこれを行うのに正しいか最良の方法であるかはわかりませんが、うまくいきました(オブジェクト指向の原則に違反していると確信していますが、私のユースケースでは他のソリューションはうまくいきませんでした)。

FragmentStatePagerAdapterでViewPagerを使用するホスティングアクティビティがありました。 FragmentStatePagerAdapterによって作成されたFragmentsへの参照を取得するために、フラグメントクラス内にコールバックインターフェイスを作成しました。

public interface Callbacks {
    public void addFragment (Fragment fragment);
    public void removeFragment (Fragment fragment);
}

ホスティングアクティビティで、インターフェイスを実装し、LinkedHasSetを作成してフラグメントを追跡しました。

public class HostingActivity extends AppCompatActivity implements ViewPagerFragment.Callbacks {

    private LinkedHashSet<Fragment> mFragments = new LinkedHashSet<>();

    @Override
    public void addFragment (Fragment fragment) {
        mFragments.add(fragment);
    }

    @Override
    public void removeFragment (Fragment fragment) {
        mFragments.remove(fragment);
    }
}

ViewPagerFragmentクラス内で、onAttach内のリストにフラグメントを追加し、onDetach内でそれらを削除しました。

public class ViewPagerFragment extends Fragment {

    private Callbacks mCallbacks;

    public interface Callbacks {
        public void addFragment (Fragment fragment);
        public void removeFragment (Fragment fragment);
    } 

    @Override
    public void onAttach (Context context) {
        super.onAttach(context);
        mCallbacks = (Callbacks) context;
        // Add this fragment to the HashSet in the hosting activity
        mCallbacks.addFragment(this);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Remove this fragment from the HashSet in the hosting activity
        mCallbacks.removeFragment(this);
        mCallbacks = null;
    }
}

ホスティングアクティビティ内で、mFragmentsを使用して、FragmentStatePagerAdapterに現在存在するフラグメントを反復処理できるようになります。

0
sbearben