109

我在让我的片段通过 相互通信时遇到问题Activity,它使用FragmentPagerAdapter,作为实现选项卡管理的帮助器类以及ViewPager与关联的连接的所有细节TabHost。我已经实现FragmentPagerAdapter了与 Android 示例项目Support4Demos提供的一样的实现。

主要问题是FragmentManager当我既没有 ID 也没有标签时,我如何才能获得特定的片段?FragmentPagerAdapter正在创建片段并自动生成 Id 和标签。

4

14 回答 14

207

问题总结

注意:在这个答案中,我将参考FragmentPagerAdapter它的源代码。但一般的解决方案也应该适用于FragmentStatePagerAdapter.

如果您正在阅读本文,您可能已经知道FragmentPagerAdapter/FragmentStatePagerAdapterFragments为您创建的ViewPager,但是在 Activity 重新创建时(无论是从设备旋转还是系统杀死您的应用程序以重新获得内存)这些Fragments都不会再次创建,而是它们的FragmentManager. 现在说你Activity需要参考这些Fragments来做他们的工作。您没有idortag为这些创建Fragments,因为FragmentPagerAdapter 在内部设置它们。所以问题是如何在没有这些信息的情况下获得对它们的引用......

当前解决方案的问题:依赖内部代码

我在这个问题和类似问题上看到的很多解决方案都依赖于Fragment通过调用FragmentManager.findFragmentByTag()和模仿内部创建的标签来获取对现有标签的引用:"android:switcher:" + viewId + ":" + id . 这样做的问题是您依赖于内部源代码,众所周知,不能保证永远保持不变。Google 的 Android 工程师可以轻松决定更改tag结构,这会破坏您的代码,使您无法找到对现有Fragments.

不依赖内部的替代解决方案tag

这是一个简单示例,说明如何获取对不依赖Fragments于. 关键是在其中覆盖和保存引用而不是在.FragmentPagerAdaptertagsFragmentsinstantiateItem()getItem()

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...
}
于 2015-03-26T01:23:30.640 回答
86

我根据以下帖子找到了我的问题的答案:在片段页面适配器中重用片段

我学到的几件事:

  1. getItem(int position)FragmentPagerAdapter这个方法实际上做了什么是相当误导的名称。它创建新的片段,而不是返回现有的片段。在这个意义上,该方法应该重命名为类似于createItem(int position)Android SDK 中的方法。所以这个方法并不能帮助我们获取碎片。
  2. 根据帖子支持 FragmentPagerAdapterholds 对旧片段的引用中的解释,您应该将片段的创建留给 ,FragmentPagerAdapter这意味着您没有对片段或其标签的引用。FragmentManager如果您有片段标签,您可以通过调用轻松检索对它的引用findFragmentByTag()。我们需要一种方法来找出给定页面位置的片段标签。

解决方案

在您的类中添加以下辅助方法以检索片段标记并将其发送到该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

于 2012-12-26T10:40:08.550 回答
18

您不需要通过手动创建片段标签来覆盖instantiateItem或依赖与内部方法的兼容性。是一个公共方法,因此您可以并且实际上应该在您的活动方法中调用它,其中包含对javadoc中描述的调用和方法的调用:makeFragmentName
instantiateItemonCreatestartUpdatefinishUpdatePagerAdapter

调用 PagerAdapter 方法 startUpdate(ViewGroup) 指示 ViewPager 的内容即将更改。随后将调用一次或多次实例化Item(ViewGroup, int) 和/或destroyItem(ViewGroup, int, Object),并通过调用finishUpdate(ViewGroup) 发出更新结束的信号。

然后,您可以通过上述方式,在需要时将片段实例的引用存储在本地变量上。参见示例:

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包围的所有选项卡,如下所示:startUpdatefinishUpdateonCreate

    adapter.startUpdate(viewPager);
    // ignoring return values of the below 2 calls, just side effects matter:
    adapter.instantiateItem(viewPager, 0);
    adapter.instantiateItem(viewPager, 1);
    adapter.finishUpdate(viewPager);

如果您不这样做,那么您将面临永远不会提交片段实例的风险FragmentManager:当您的活动变为前台instantiateItem时将自动调用以获取您的片段,但startUpdate/finishUpdate 可能 不会(取决于实现细节)以及它们基本上是什么做的是开始/提交一个FragmentTransaction.
可能会导致对已创建片段实例的引用很快丢失(例如,当您旋转屏幕时)并且重新创建的频率比必要的多得多。根据您的片段的“重”程度,它可能会产生不可忽略的性能后果。
此外,在这种情况下,存储在本地变量上的片段实例可能变得陈旧:如果 android 平台尝试从FragmentManager任何原因获取它们,它将失败,因此将创建和使用新的,而您的 vars 仍将引用旧的。

于 2016-12-27T12:31:56.780 回答
13

我这样做的方式是定义一个 WeakReferences 的哈希表,如下所示:

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 的实现方式。当然如果fragment已经被FragmentPagerAdapter释放或者还没有被创建,getFragment会返回null。

如果有人发现这种方法有问题,我们非常欢迎发表评论。

于 2014-05-24T10:10:25.250 回答
10

我创建了这个方法,它可以让我获得对当前片段的引用。

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;
}
于 2013-03-14T11:31:49.080 回答
5

ViewPager2更新的 FragmentStateAdapter

FragmentStateAdaptercreateFragment()而不是getItem(). 并且没有这样的方法instantiateView()

因此,在配置更改后重新创建托管Activity/时,不会被调用。这意味着适配器中的片段不会再次创建,而是从. 所以如果你需要对片段做一些工作,你可以简单地从.FragmentcreateFragment()FragmentManagerFragmentManager

适配器创建:

CustomFragmentAdapter adapter = new CustomFragmentAdapter(getChildFragmentManager(), getLifecycle());

FragmentManager从after Activity/ Fragmentreload中检索片段:

    FragmentManager manager = getChildFragmentManager();
    ArrayList<Fragment> fragments = new ArrayList<>(manager.getFragments());

    for (Fragment fr : fragments) {
        
        // do something
    }
于 2021-06-20T16:11:31.860 回答
2

@personne3000 建议的解决方案很好,但它有一个问题:当活动进入后台并被系统杀死(为了获得一些空闲内存)然后恢复时,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;
    }
}
于 2014-08-27T08:34:30.670 回答
2

获取片段句柄的主要障碍是您不能依赖 getItem()。方向更改后,对片段的引用将为空,并且不会再次调用 getItem()。

这是一种不依赖 FragmentPagerAdapter 的实现来获取标签的方法。覆盖 instanceiteItem() ,它将返回从 getItem() 创建的或从片段管理器中找到的片段。

@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;
}
于 2015-06-22T20:16:21.040 回答
0

请参阅这篇关于从 FragmentPagerAdapter 返回片段的帖子。是否依赖于您知道片段的索引 - 但这将在 getItem() 中设置(仅在实例化时)

于 2013-01-22T20:22:34.110 回答
0

我设法通过使用 ids 而不是标签来解决这个问题。(我正在使用我定义的 FragmentStatePagerAdapter ,它使用我的自定义片段,其中我覆盖了 onAttach 方法,您将 id 保存在某处:

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

然后您只需在活动中轻松访问片段:

Fragment f = getSupportFragmentManager.findFragmentById(fragId);
于 2015-10-03T15:01:44.690 回答
0

我不知道这是否是最好的方法,但没有其他方法对我有用。包括 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);
}

这种方法将实际可见片段附加到活动而不创建新片段。

于 2017-02-17T16:54:37.257 回答
0

不确定我的方法是否是正确的或最好的方法,因为我是 Java/Android 的相对初学者,但它确实有效(我确信它违反了面向对象的原则,但没有其他解决方案适用于我的用例)。

我有一个使用 ViewPager 和 FragmentStatePagerAdapter 的托管活动。为了获得对 FragmentStatePagerAdapter 创建的片段的引用,我在片段类中创建了一个回调接口:

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 中当前存在的片段。

于 2018-05-21T01:19:29.460 回答
0

这个类不依赖内部标签就可以解决问题。警告:片段应该使用 getFragment 方法而不是 getItem 方法来访问。

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);
    }
}
于 2018-08-30T11:58:48.213 回答
-6

继续尝试这段代码,

public class MYFragmentPAdp extends FragmentPagerAdapter {

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

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

     @Override
     public Fragment getItem(int position) {
         if (position == 0)
             Fragment fragment = new Fragment1();
         else (position == 1)
             Fragment fragment = new Fragment2();
         return fragment;
     }
}
于 2012-12-26T11:03:21.963 回答