ViewPager和片段 - 什麼是存儲片段狀態的正確方法?

本文翻譯自:ViewPager and fragments — what's the right way to store fragment's state?

Fragments seem to be very nice for separation of UI logic into some modules. 片段似乎非常適合將UI邏輯分離爲某些模塊。 But along with ViewPager its lifecycle is still misty to me. 但與ViewPager一起,它的生命週期對我來說仍然是迷霧。 So Guru thoughts are badly needed! 因此迫切需要大師的想法!

Edit 編輯

See dumb solution below ;-) 見下面的啞解決方案;-)

Scope 範圍

Main activity has a ViewPager with fragments. 主要活動有一個帶有片段的ViewPager 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!... 第一次發佈時一切正常,但是!...

Problem 問題

When the activity gets recreated (eg on orientation change) so do the ViewPager 's fragments. 當重新創建活動時(例如,在方向改變時), ViewPager的片段也是如此。 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. 代碼(你會在下面找到)說每次創建活動時我都會嘗試創建一個ViewPager片段相同的新ViewPager片段適配器(也許這就是問題)但FragmentManager已經將所有這些片段存儲在某個地方(其中?)並啓動那些娛樂機制。 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. 因此,重新啓動機制使用我的回調接口調用來調用“舊”片段的onAttach,onCreateView等,以通過Activity的實現方法啓動數據。 But this method points to the newly created fragment which is created via the Activity's onCreate method. 但是這個方法指向通過Activity的onCreate方法創建的新創建的片段。

Issue 問題

Maybe I'm using wrong patterns but even Android 3 Pro book doesn't have much about it. 也許我使用了錯誤的模式,但即便是Android 3 Pro也沒有太多關於它的內容。 So, please , give me one-two punch and point out how to do it the right way. 所以, 給我一兩拳,並指出如何以正確的方式做到這一點。 Many thanks! 非常感謝!

Code

Main Activity 主要活動

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 aka helper BasePagerActivity又名助手

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}

Adapter 適配器

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
}

}

Fragment 分段

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

Solution

The dumb solution is to save the fragments inside onSaveInstanceState (of host Activity) with putFragment and get them inside onCreate via getFragment. 愚蠢的解決方案是使用putFragment將片段保存在onSaveInstanceState(主機Activity)中,並通過getFragment將它們放在onCreate中。 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();
    ...
}

#1樓

參考:https://stackoom.com/question/XMbi/ViewPager和片段-什麼是存儲片段狀態的正確方法


#2樓

I want to offer an alternate solution for perhaps a slightly different case, since many of my searches for answers kept leading me to this thread. 我想爲一個稍微不同的案例提供一個替代解決方案,因爲我的許多搜索答案一直引導我到這個線程。

My case - I'm creating/adding pages dynamically and sliding them into a ViewPager, but when rotated (onConfigurationChange) I end up with a new page because of course OnCreate is called again. 我的情況 - 我正在動態創建/添加頁面並將它們滑動到ViewPager中,但是當旋轉(onConfigurationChange)時,我最終得到一個新頁面,因爲當然再次調用OnCreate。 But I want to keep reference to all the pages that were created prior to the rotation. 但我想繼續引用旋轉之前創建的所有頁面。

Problem - I don't have unique identifiers for each fragment I create, so the only way to reference was to somehow store references in an Array to be restored after the rotation/configuration change. 問題 - 我沒有爲我創建的每個片段提供唯一標識符,因此引用的唯一方法是以某種方式存儲在旋轉/配置更改後要恢復的數組中的引用。

Workaround - The key concept was to have the Activity (which displays the Fragments) also manage the array of references to existing Fragments, since this activity can utilize Bundles in onSaveInstanceState 解決方法 - 關鍵概念是讓Activity(顯示片段)也管理對現有片段的引用數組,因爲此活動可以使用onSaveInstanceState中的Bundles

public class MainActivity extends FragmentActivity

So within this Activity, I declare a private member to track the open pages 因此,在此活動中,我聲明一個私人成員來跟蹤打開的頁面

private List<Fragment> retainedPages = new ArrayList<Fragment>();

This is updated everytime onSaveInstanceState is called and restored in onCreate 每次在onCreate中調用並恢復onSaveInstanceState時都會更新

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

...so once it's stored, it can be retrieved... ...所以一旦它被存儲,它可以被檢索...

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

These were the necessary changes to the main activity, and so I needed the members and methods within my FragmentPagerAdapter for this to work, so within 這些是對主要活動的必要更改,因此我需要FragmentPagerAdapter中的成員和方法才能使其工作,因此

public class ViewPagerAdapter extends FragmentPagerAdapter

an identical construct (as shown above in MainActivity ) 一個相同的結構(如上面的MainActivity中所示)

private List<Fragment> _pages = new ArrayList<Fragment>();

and this syncing (as used above in onSaveInstanceState) is supported specifically by the methods 並且這種方法特別支持這種同步(如上面onSaveInstanceState中所使用的)

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

public void importList(List<Fragment> savedPages) {
    _pages = savedPages;
}

And then finally, in the fragment class 最後,在片段類中

public class CustomFragment extends Fragment

in order for all this to work, there were two changes, first 爲了使所有這些工作,首先有兩個變化

public class CustomFragment extends Fragment implements Serializable

and then adding this to onCreate so Fragments aren't destroyed 然後將其添加到onCreate中,以便碎片不會被破壞

setRetainInstance(true);

I'm still in the process of wrapping my head around Fragments and Android life cycle, so caveat here is there may be redundancies/inefficiencies in this method. 我仍然處於圍繞Fragments和Android生命週期的過程中,所以請注意,這種方法可能存在冗餘/效率低下。 But it works for me and I hope might be helpful for others with cases similar to mine. 但它適用於我,我希望可能對其他類似我的案例有幫助。


#3樓

I found another relatively easy solution for your question. 我找到了另一個相對簡單的解決方案。

As you can see from the FragmentPagerAdapter source code , the fragments managed by FragmentPagerAdapter store in the FragmentManager under the tag generated using: 正如您從FragmentPagerAdapter源代碼中看到的, FragmentPagerAdapter管理的FragmentPagerAdapter存儲在FragmentManager下使用以下標記生成的標記:

String tag="android:switcher:" + viewId + ":" + index;

The viewId is the container.getId() , the container is your ViewPager instance. viewIdcontainer.getId()container是您的ViewPager實例。 The index is the position of the fragment. index是片段的位置。 Hence you can save the object id to the outState : 因此,您可以將對象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);

If you want to communicate with this fragment, you can get if from FragmentManager , such as: 如果要與此片段進行通信,可以從FragmentManager獲取,例如:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")

#4樓

My solution is very rude but works: being my fragments dynamically created from retained data, I simply remove all fragment from the PageAdapter before calling super.onSaveInstanceState() and then recreate them on activity creation: 我的解決方案非常粗魯但有效:作爲我從保留數據動態創建的片段,我只需在調用super.onSaveInstanceState()之前從PageAdapter刪除所有片段,然後在創建活動時重新創建它們:

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

You can't remove them in onDestroy() , otherwise you get this exception: 您無法在onDestroy()刪除它們,否則您會收到此異常:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState java.lang.IllegalStateException:無法在onSaveInstanceState之後執行此操作

Here the code in the page adapter: 這裏是頁面適配器中的代碼:

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

I only save the current page and restore it in onCreate() , after the fragments have been created. 我只保存當前頁面並在創建片段後在onCreate()恢復它。

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  

#5樓

add: 加:

   @SuppressLint("ValidFragment")

before your class. 在你上課之前。

it it doesn´t work do something like this: 它不起作用做這樣的事情:

@SuppressLint({ "ValidFragment", "HandlerLeak" })

#6樓

I want to offer a solution that expands on antonyt 's wonderful answer and mention of overriding FragmentPageAdapter.instantiateItem(View, int) to save references to created Fragments so you can do work on them later. 我想提供一個解決方案,擴展antonyt精彩答案並提及重寫FragmentPageAdapter.instantiateItem(View, int)以保存對創建的Fragments引用,以便您以後可以對它們進行處理。 This should also work with FragmentStatePagerAdapter ; 這也適用於FragmentStatePagerAdapter ; see notes for details. 請參閱說明了解詳情。


Here's a simple example of how to get a reference to the Fragments returned by FragmentPagerAdapter that doesn't rely on the internal tags set on the Fragments . 下面是如何獲得的一個參考一個簡單的例子Fragments由歸國FragmentPagerAdapter不依賴於內部tags上所設置的Fragments The key is to override instantiateItem() and save references in there instead of in getItem() . 關鍵是覆蓋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();
        }
    }
}

or if you prefer to work with tags instead of class member variables/references to the Fragments you can also grab the tags set by FragmentPagerAdapter in the same manner: NOTE: this doesn't apply to FragmentStatePagerAdapter since it doesn't set tags when creating its Fragments . 或者如果您更喜歡使用tags而不是類成員變量/對Fragments引用,您也可以以相同的方式獲取FragmentPagerAdapter設置的tags :注意:這不適用於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;
}

Note that this method does NOT rely on mimicking the internal tag set by FragmentPagerAdapter and instead uses proper APIs for retrieving them. 請注意,此方法不依賴於模仿FragmentPagerAdapter設置的內部tag ,而是使用適當的API來檢索它們。 This way even if the tag changes in future versions of the SupportLibrary you'll still be safe. 這樣,即使tagSupportLibrary未來版本中發生變化,您仍然可以安全。


Don't forget that depending on the design of your Activity , the Fragments you're trying to work on may or may not exist yet, so you have to account for that by doing null checks before using your references. 不要忘記 ,根據您的Activity的設計,您嘗試處理的Fragments可能存在,也可能不存在,因此您必須在使用引用之前進行null檢查以解決這個問題。

Also, if instead you're working with FragmentStatePagerAdapter , then you don't want to keep hard references to your Fragments because you might have many of them and hard references would unnecessarily keep them in memory. 另外,如果您正在使用FragmentStatePagerAdapter ,那麼您不希望保留對Fragments硬引用,因爲您可能擁有其中的許多內容,並且硬引用會不必要地將它們保留在內存中。 Instead save the Fragment references in WeakReference variables instead of standard ones. 而是在WeakReference變量中保存Fragment引用而不是標準變量。 Like this: 像這樣:

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...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章