ViewPager and fragments — what's the right way to store fragment's state?

后端 未结 11 1320
遇见更好的自我
遇见更好的自我 2020-11-22 02:21

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 b

相关标签:
11条回答
  • 2020-11-22 02:28

    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. 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

    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

    @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

    public class ViewPagerAdapter extends FragmentPagerAdapter
    

    an identical construct (as shown above in MainActivity )

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

    and this syncing (as used above in onSaveInstanceState) is supported specifically by the methods

    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

    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. But it works for me and I hope might be helpful for others with cases similar to mine.

    0 讨论(0)
  • 2020-11-22 02:32

    add:

       @SuppressLint("ValidFragment")
    

    before your class.

    it it doesn´t work do something like this:

    @SuppressLint({ "ValidFragment", "HandlerLeak" })
    
    0 讨论(0)
  • 2020-11-22 02:35

    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. This should also work with 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. The key is to override instantiateItem() and save references in there instead of in 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.

    @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. This way even if the tag changes in future versions of the SupportLibrary you'll still be safe.


    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.

    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. Instead save the Fragment references in WeakReference variables instead of standard ones. 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...
    }
    
    0 讨论(0)
  • 2020-11-22 02:36

    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:

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

    The viewId is the container.getId(), the container is your ViewPager instance. The index is the position of the fragment. Hence you can save the object id to the 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:

    getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")
    
    0 讨论(0)
  • 2020-11-22 02:38

    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.

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

    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.

    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.

    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).

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

    0 讨论(0)
  • 2020-11-22 02:41

    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:

    @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:

    java.lang.IllegalStateException: Can not perform this action after 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.

    if (savedInstanceState != null)
        mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  
    
    0 讨论(0)
提交回复
热议问题