ViewPager 和片段——存储片段状态的正确方法是什么? [英] ViewPager and fragments — what's the right way to store fragment's state?

查看:30
本文介绍了ViewPager 和片段——存储片段状态的正确方法是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Fragment 似乎非常适合将 UI 逻辑分离到一些模块中.但是与 ViewPager 一起,它的生命周期对我来说仍然是模糊的.所以上师的思想是非常需要的!

Fragments seem to be very nice for separation of UI logic into some modules. But along with ViewPager its lifecycle is still misty to me. So Guru thoughts are badly needed!

请参阅下面的愚蠢解决方案;-)

See dumb solution below ;-)

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

Main activity has a ViewPager with fragments. Those fragments could implement a little bit different logic for other (submain) activities, so the fragments' data is filled via a callback interface inside the activity. And everything works fine on first launch, but!...

当活动被重新创建时(例如在方向改变时),ViewPager 的片段也会被重新创建.代码(您将在下面找到)说,每次创建活动时,我都会尝试创建一个与片段相同的新 ViewPager 片段适配器(也许这是问题所在)但 FragmentManager 已经拥有所有这些存储在某处(哪里?)的片段并启动这些片段的娱乐机制.因此,娱乐机制通过我的回调接口调用旧"片段的 onAttach、onCreateView 等,以通过 Activity 的实现方法启动数据.但是这个方法指向的是通过Activity的onCreate方法创建的新创建的fragment.

When the activity gets recreated (e.g. on orientation change) so do the ViewPager's fragments. The code (you'll find below) says that every time the activity is created I try to create a new ViewPager fragments adapter the same as fragments (maybe this is the problem) but FragmentManager already has all these fragments stored somewhere (where?) and starts the recreation mechanism for those. So the recreation mechanism calls the "old" fragment's onAttach, onCreateView, etc. with my callback interface call for initiating data via the Activity's implemented method. But this method points to the newly created fragment which is created via the Activity's onCreate method.

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

Maybe I'm using wrong patterns but even Android 3 Pro book doesn't have much about it. So, please, give me one-two punch and point out how to do it the right way. Many thanks!

主要活动

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);
}

BasePagerActivity 又名助手

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}

适配器

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
}

}

片段

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... */
}

解决方案

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

Solution

The dumb solution is to save the fragments inside onSaveInstanceState (of host Activity) with putFragment and get them inside onCreate via getFragment. But I still have a strange feeling that things shouldn't work like that... See code below:

    @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();
    ...
}

推荐答案

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

When the FragmentPagerAdapter adds a fragment to the FragmentManager, it uses a special tag based on the particular position that the fragment will be placed. FragmentPagerAdapter.getItem(int position) is only called when a fragment for that position does not exist. After rotating, Android will notice that it already created/saved a fragment for this particular position and so it simply tries to reconnect with it with FragmentManager.findFragmentByTag(), instead of creating a new one. All of this comes free when using the FragmentPagerAdapter and is why it is usual to have your fragment initialisation code inside the getItem(int) method.

即使我们没有使用 FragmentPagerAdapter,在 Activity.onCreate(Bundle) 中每次都创建一个新片段也不是一个好主意.正如你所看到的,当一个片段被添加到 FragmentManager 时,它会在旋转后为你重新创建,不需要再次添加它.这样做是处理片段时出错的常见原因.

Even if we were not using a FragmentPagerAdapter, it is not a good idea to create a new fragment every single time in Activity.onCreate(Bundle). As you have noticed, when a fragment is added to the FragmentManager, it will be recreated for you after rotating and there is no need to add it again. Doing so is a common cause of errors when working with fragments.

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

A usual approach when working with fragments is this:

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(); 
    }

    ...

}

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

When using a FragmentPagerAdapter, we relinquish fragment management to the adapter, and do not have to perform the above steps. By default, it will only preload one Fragment in front and behind the current position (although it does not destroy them unless you are using FragmentStatePagerAdapter). This is controlled by ViewPager.setOffscreenPageLimit(int). Because of this, directly calling methods on the fragments outside of the adapter is not guaranteed to be valid, because they may not even be alive.

长话短说,您使用 putFragment 之后能够获得引用的解决方案并不那么疯狂,并且与使用片段的正常方式(上图)没什么不同.否则很难获得引用,因为片段是由适配器添加的,而不是您个人添加的.只需确保 offscreenPageLimit 足够高以始终加载所需的片段,因为您依赖于它的存在.这绕过了 ViewPager 的延迟加载功能,但似乎是您对应用程序的期望.

To cut a long story short, your solution to use putFragment to be able to get a reference afterwards is not so crazy, and not so unlike the normal way to use fragments anyway (above). It is difficult to obtain a reference otherwise because the fragment is added by the adapter, and not you personally. Just make sure that the offscreenPageLimit is high enough to load your desired fragments at all times, since you rely on it being present. This bypasses lazy loading capabilities of the ViewPager, but seems to be what you desire for your application.

另一种方法是覆盖 FragmentPageAdapter.instantiateItem(View, int) 并在返回之前保存对从超级调用返回的片段的引用(它具有查找片段的逻辑,如果已经存在).

Another approach is to override FragmentPageAdapter.instantiateItem(View, int) and save a reference to the fragment returned from the super call before returning it (it has the logic to find the fragment, if already present).

要获得更完整的图片,请查看 FragmentPagerAdapter(短)和 ViewPager (长).

For a fuller picture, have a look at some of the source of FragmentPagerAdapter (short) and ViewPager (long).

这篇关于ViewPager 和片段——存储片段状态的正确方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆