ViewPager和片段 - 什么是存储片段状态的正确方法?

Ale*_*niy 483 android design-patterns android-fragments android-viewpager

片段似乎非常适合将UI逻辑分离为某些模块.但随着ViewPager它的生命周期对我来说仍然是迷雾.因此迫切需要大师的想法!

编辑

见下面的哑解决方案;-)

范围

主要活动有ViewPager碎片.这些片段可以为其他(子域)活动实现一些不同的逻辑,因此片段的数据通过活动内部的回调接口填充.第一次发布时一切正常,但是!...

问题

当重新创建活动时(例如,在方向改变时),那么ViewPager碎片也是如此.代码(你会在下面找到)说每次创建活动时我尝试创建一个ViewPager与片段相同的新片段适配器(也许这就是问题)但FragmentManager已经将所有这些片段存储在某个地方(其中?)和为那些人启动娱乐机制.因此,重新启动机制使用我的回调接口调用来调用"旧"片段的onAttach,onCreateView等,以通过Activity的实现方法启动数据.但是这个方法指向通过Activity的onCreate方法创建的新创建的片段.

问题

也许我使用了错误的模式,但即便是Android 3 Pro也没有太多关于它的内容.所以,给我一两拳,并指出如何以正确的方式做到这一点.非常感谢!

主要活动

public class DashboardActivity extends BasePagerActivity implements OnMessageListActionListener {

private MessagesFragment mMessagesFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    setContentView(R.layout.viewpager_container);
    new DefaultToolbar(this);

    // create fragments to use
    mMessagesFragment = new MessagesFragment();
    mStreamsFragment = new StreamsFragment();

    // set titles and fragments for view pager
    Map<String, Fragment> screens = new LinkedHashMap<String, Fragment>();
    screens.put(getApplicationContext().getString(R.string.dashboard_title_dumb), new DumbFragment());
    screens.put(getApplicationContext().getString(R.string.dashboard_title_messages), mMessagesFragment);

    // instantiate view pager via adapter
    mPager = (ViewPager) findViewById(R.id.viewpager_pager);
    mPagerAdapter = new BasePagerAdapter(screens, getSupportFragmentManager());
    mPager.setAdapter(mPagerAdapter);

    // set title indicator
    TitlePageIndicator indicator = (TitlePageIndicator) findViewById(R.id.viewpager_titles);
    indicator.setViewPager(mPager, 1);

}

/* set of fragments callback interface implementations */

@Override
public void onMessageInitialisation() {

    Logger.d("Dash onMessageInitialisation");
    if (mMessagesFragment != null)
        mMessagesFragment.loadLastMessages();
}

@Override
public void onMessageSelected(Message selectedMessage) {

    Intent intent = new Intent(this, StreamActivity.class);
    intent.putExtra(Message.class.getName(), selectedMessage);
    startActivity(intent);
}
Run Code Online (Sandbox Code Playgroud)

BasePagerActivity又名助手

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}
Run Code Online (Sandbox Code Playgroud)

适配器

public class BasePagerAdapter extends FragmentPagerAdapter implements TitleProvider {

private Map<String, Fragment> mScreens;

public BasePagerAdapter(Map<String, Fragment> screenMap, FragmentManager fm) {

    super(fm);
    this.mScreens = screenMap;
}

@Override
public Fragment getItem(int position) {

    return mScreens.values().toArray(new Fragment[mScreens.size()])[position];
}

@Override
public int getCount() {

    return mScreens.size();
}

@Override
public String getTitle(int position) {

    return mScreens.keySet().toArray(new String[mScreens.size()])[position];
}

// hack. we don't want to destroy our fragments and re-initiate them after
@Override
public void destroyItem(View container, int position, Object object) {

    // TODO Auto-generated method stub
}

}
Run Code Online (Sandbox Code Playgroud)

分段

public class MessagesFragment extends ListFragment {

private boolean mIsLastMessages;

private List<Message> mMessagesList;
private MessageArrayAdapter mAdapter;

private LoadMessagesTask mLoadMessagesTask;
private OnMessageListActionListener mListener;

// define callback interface
public interface OnMessageListActionListener {
    public void onMessageInitialisation();
    public void onMessageSelected(Message selectedMessage);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    // setting callback
    mListener = (OnMessageListActionListener) activity;
    mIsLastMessages = activity instanceof DashboardActivity;

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    inflater.inflate(R.layout.fragment_listview, container);
    mProgressView = inflater.inflate(R.layout.listrow_progress, null);
    mEmptyView = inflater.inflate(R.layout.fragment_nodata, null);
    return super.onCreateView(inflater, container, savedInstanceState);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    // instantiate loading task
    mLoadMessagesTask = new LoadMessagesTask();

    // instantiate list of messages
    mMessagesList = new ArrayList<Message>();
    mAdapter = new MessageArrayAdapter(getActivity(), mMessagesList);
    setListAdapter(mAdapter);
}

@Override
public void onResume() {
    mListener.onMessageInitialisation();
    super.onResume();
}

public void onListItemClick(ListView l, View v, int position, long id) {
    Message selectedMessage = (Message) getListAdapter().getItem(position);
    mListener.onMessageSelected(selectedMessage);
    super.onListItemClick(l, v, position, id);
}

/* public methods to load messages from host acitivity, etc... */
}
Run Code Online (Sandbox Code Playgroud)

愚蠢的解决方案是使用putFragment将片段保存在onSaveInstanceState(主机Activity)中,并通过getFragment将它们放在onCreate中.但我仍然有一种奇怪的感觉,事情不应该像那样......请参阅下面的代码:

    @Override
protected void onSaveInstanceState(Bundle outState) {

    super.onSaveInstanceState(outState);
    getSupportFragmentManager()
            .putFragment(outState, MessagesFragment.class.getName(), mMessagesFragment);
}

protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    ...
    // create fragments to use
    if (savedInstanceState != null) {
        mMessagesFragment = (MessagesFragment) getSupportFragmentManager().getFragment(
                savedInstanceState, MessagesFragment.class.getName());
                StreamsFragment.class.getName());
    }
    if (mMessagesFragment == null)
        mMessagesFragment = new MessagesFragment();
    ...
}
Run Code Online (Sandbox Code Playgroud)

ant*_*nyt 446

FragmentPagerAdapter增加一个片段到FragmentManager,它采用了基于该片段将被放置在特定位置上的特殊标记.FragmentPagerAdapter.getItem(int position)仅在该位置的片段不存在时调用.在旋转之后,Android会注意到它已经为这个特定位置创建/保存了一个片段,因此它只是尝试与之重新连接FragmentManager.findFragmentByTag(),而不是创建一个新片段.所有这些在使用时都是免费的,这FragmentPagerAdapter就是为什么通常在方法中包含片段初始化代码getItem(int).

即使我们没有使用a FragmentPagerAdapter,每次创建一个新片段也不是一个好主意Activity.onCreate(Bundle).正如您所注意到的,当片段添加到FragmentManager时,它将在旋转后为您重新创建,无需再次添加.在处理片段时,这样做是导致错误的常见原因.

处理片段时常用的方法是:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ...

    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit(); 
    }

    ...

}
Run Code Online (Sandbox Code Playgroud)

使用a时FragmentPagerAdapter,我们将片段管理放弃到适配器,而不必执行上述步骤.默认情况下,它只会在当前位置的前面和后面预加载一个片段(尽管除非你使用它,否则它不会销毁它们FragmentStatePagerAdapter).这由ViewPager.setOffscreenPageLimit(int)控制.因此,不能保证在适配器外部的片段上直接调用方法是有效的,因为它们甚至可能不存在.

简而言之,你的解决方案用于putFragment能够在事后获得参考并不是那么疯狂,并且与正常使用片段的方式不同(上图).否则很难获得参考,因为片段是由适配器添加的,而不是您个人添加的.只要确保它offscreenPageLimit足够高,可以随时加载您想要的碎片,因为您依赖它存在.这绕过了ViewPager的延迟加载功能,但似乎是您对应用程序的期望.

另一种方法是重写FragmentPageAdapter.instantiateItem(View, int)并保存到返回之前从超级调用返回的片段的引用(它必须找到片段中的逻辑,如果已经存在).

要获得更全面的图片,请查看FragmentPagerAdapter(简短)和ViewPager(long)的一些来源.

  • 辉煌!现在事情变得清洁了.非常感谢. (13认同)
  • ViewPager.setOffscreenPageLimit(int)的+1 (13认同)
  • 喜欢最后一部分.有片段的缓存并将缓存逻辑中的put放在`FragmentPageAdapter.instantiateItem(View,int)`中.最后修复了一个持久的bug,它只出现在旋转/配置变化中并且让我发疯... (7认同)
  • 这解决了我在轮换方面遇到的问题.也许我只是看不出来看,但这是有记录的吗?即使用标签从以前的状态恢复?这可能是一个明显的答案,但我对Android开发还很陌生. (2认同)

Ton*_*han 36

我想提供一个可扩展的解决方案antonyt精彩的回答和压倒一切的提FragmentPageAdapter.instantiateItem(View, int)保存到创建的引用Fragments,所以你可以对他们以后做的工作.这应该也适用FragmentStatePagerAdapter; 请参阅说明了解详情.


这是一个简单的例子,说明如何获取对Fragments返回的引用FragmentPagerAdapter,而不依赖于内部tagsFragments.关键是instantiateItem()在那里覆盖和保存引用而不是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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

或者如果您更喜欢使用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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,此方法不依赖于模仿内部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...
}
Run Code Online (Sandbox Code Playgroud)


小智 18

我找到了另一个相对简单的解决方案.

正如您从FragmentPagerAdapter源代码中看到的那样,由FragmentPagerAdapter商店管理的片段在使用FragmentManager以下标签生成的标签下:

String tag="android:switcher:" + viewId + ":" + index;
Run Code Online (Sandbox Code Playgroud)

viewIdcontainer.getId(),该container是你的ViewPager实例.这index是片段的位置.因此,您可以将对象ID保存到outState:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("viewpagerid" , mViewPager.getId() );
}

@Override
    protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null)
        viewpagerid=savedInstanceState.getInt("viewpagerid", -1 );  

    MyFragmentPagerAdapter titleAdapter = new MyFragmentPagerAdapter (getSupportFragmentManager() , this);        
    mViewPager = (ViewPager) findViewById(R.id.pager);
    if (viewpagerid != -1 ){
        mViewPager.setId(viewpagerid);
    }else{
        viewpagerid=mViewPager.getId();
    }
    mViewPager.setAdapter(titleAdapter);
Run Code Online (Sandbox Code Playgroud)

如果你想与这个片段进行通信,你可以从中获取FragmentManager,例如:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")
Run Code Online (Sandbox Code Playgroud)

  • 嗯,我不认为这是一个很好的方式去回答内部标签命名约定,永远不会永远保持不变. (21认同)

Fra*_*Yin 16

我想为一个稍微不同的案例提供一个替代解决方案,因为我的许多搜索答案一直引导我到这个线程.

我的情况 - 我正在动态创建/添加页面并将它们滑动到ViewPager中,但是当旋转(onConfigurationChange)时,我最终得到一个新页面,因为当然再次调用OnCreate.但我想继续引用旋转之前创建的所有页面.

问题 - 我没有为我创建的每个片段提供唯一标识符,因此引用的唯一方法是以某种方式存储在旋转/配置更改后要恢复的数组中的引用.

解决方法 - 关键概念是让Activity(显示片段)也管理对现有片段的引用数组,因为此活动可以使用onSaveInstanceState中的Bundles

public class MainActivity extends FragmentActivity
Run Code Online (Sandbox Code Playgroud)

因此,在此活动中,我声明一个私人成员来跟踪打开的页面

private List<Fragment> retainedPages = new ArrayList<Fragment>();
Run Code Online (Sandbox Code Playgroud)

每次在onCreate中调用并恢复onSaveInstanceState时都会更新

@Override
protected void onSaveInstanceState(Bundle outState) {
    retainedPages = _adapter.exportList();
    outState.putSerializable("retainedPages", (Serializable) retainedPages);
    super.onSaveInstanceState(outState);
}
Run Code Online (Sandbox Code Playgroud)

...所以一旦它被存储,它可以被检索...

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if (savedInstanceState != null) {
        retainedPages = (List<Fragment>) savedInstanceState.getSerializable("retainedPages");
    }
    _mViewPager = (CustomViewPager) findViewById(R.id.viewPager);
    _adapter = new ViewPagerAdapter(getApplicationContext(), getSupportFragmentManager());
    if (retainedPages.size() > 0) {
        _adapter.importList(retainedPages);
    }
    _mViewPager.setAdapter(_adapter);
    _mViewPager.setCurrentItem(_adapter.getCount()-1);
}
Run Code Online (Sandbox Code Playgroud)

这些是对主要活动的必要更改,因此我需要FragmentPagerAdapter中的成员和方法才能使其工作,因此

public class ViewPagerAdapter extends FragmentPagerAdapter
Run Code Online (Sandbox Code Playgroud)

一个相同的结构(如上面的MainActivity中所示)

private List<Fragment> _pages = new ArrayList<Fragment>();
Run Code Online (Sandbox Code Playgroud)

并且这种方法特别支持这种同步(如上面onSaveInstanceState中所使用的)

public List<Fragment> exportList() {
    return _pages;
}

public void importList(List<Fragment> savedPages) {
    _pages = savedPages;
}
Run Code Online (Sandbox Code Playgroud)

最后,在片段类中

public class CustomFragment extends Fragment
Run Code Online (Sandbox Code Playgroud)

为了使所有这些工作,首先有两个变化

public class CustomFragment extends Fragment implements Serializable
Run Code Online (Sandbox Code Playgroud)

然后将其添加到onCreate中,以便碎片不会被破坏

setRetainInstance(true);
Run Code Online (Sandbox Code Playgroud)

我仍然处于围绕Fragments和Android生命周期的过程中,所以请注意,这种方法可能存在冗余/效率低下.但它适用于我,我希望可能对其他类似我的案例有帮助.

  • 谢谢你的代码对我很有帮助. (2认同)
  • +1 for setRetainInstance(true) - 我正在寻找的黑魔法! (2认同)

小智 8

我的解决方案非常粗鲁但有效:作为我从保留数据动态创建的片段,我只需从PageAdapter调用之前删除所有片段super.onSaveInstanceState(),然后在创建活动时重新创建它们:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
    mSectionsPagerAdapter.removeAllfragments();
    super.onSaveInstanceState(outState);
}
Run Code Online (Sandbox Code Playgroud)

你不能删除它们onDestroy(),否则你会得到这个例外:

java.lang.IllegalStateException: 之后无法执行此操作 onSaveInstanceState

这里是页面适配器中的代码:

public void removeAllfragments()
{
    if ( mFragmentList != null ) {
        for ( Fragment fragment : mFragmentList ) {
            mFm.beginTransaction().remove(fragment).commit();
        }
        mFragmentList.clear();
        notifyDataSetChanged();
    }
}
Run Code Online (Sandbox Code Playgroud)

onCreate()在创建片段后,我只保存当前页面并将其恢复.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  
Run Code Online (Sandbox Code Playgroud)