71

I've been seeing some strange behavior with my ViewPager along with my own FragmentStatePagerAdapter.

My View hierarchy goes like this:

-> (1) Fragment root view (RelativeLayout)
 -> (2) ViewPager
  -> (3) ViewPager's current fragment view

When the Fragment that is responsible for the Fragment root view (1) gets hidden (using .hide() in a fragment transaction) and then shown (with .show()), the fragment view that was currently showing in the ViewPager (3) becomes null, although the fragment still exists. Basically, my ViewPager becomes completely blank/transparent.

The only way I have found to fix this is to call

int current = myViewPager.getCurrentItem();
myViewPager.setAdapter(myAdapter);
myViewPager.setCurrentItem(current);

after the parent fragment is shown. This somehow triggers the views to be recreated and appear on screen. Unfortunately, this occasionally causes exceptions dealing with the pager adapter calling unregisterDataSetObserver() twice on an old observer.

Is there a better way to do this? I guess what I am asking is:

Why are my fragment views inside my ViewPager getting destroyed when the parent fragment of the ViewPager is hidden?

Update: this also happens when the application is "minimized" and then "restored" (by pressing the home action key and then returning).

Per request, here's my pager adapter class:

public class MyInfoSlidePagerAdapter extends FragmentStatePagerAdapter {

    private ArrayList<MyInfo> infos = new ArrayList<MyInfo>();

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

    public MyInfoSlidePagerAdapter (FragmentManager fm, MyInfo[] newInfos) {
        super(fm);
        setInfos(newInfos);
    }

    @Override
    public int getItemPosition(Object object) {
        int position = infos.indexOf(((MyInfoDetailsFragment)object).getMyInfo());
        return position > 0 ? position : POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return infos.get(position).getName();
    }

    @Override
    public Fragment getItem(int i) {
        return infos.size() > 0 ? MyInfoDetailsFragment.getNewInstance(infos.get(i)) : null;
    }

    @Override
    public int getCount() {
        return infos.size();
    }

    public Location getMyInfoAtPosition(int i) {
        return infos.get(i);
    }

    public void setInfos(MyInfo[] newInfos) {
        infos = new ArrayList<MyInfo>(Arrays.asList(newInfos));
    }

    public int getPositionOfMyInfo(MyInfo info) {
        return infos.indexOf(info);
    }
}

I've renamed some variables but other than that it is exactly what I have.

John Leehey
  • 22,052
  • 8
  • 61
  • 88
  • after the 'hide' / 'show' ... could try calling 'notifyDatasetChanged' on the adapter... that may avoid your issues with dupe calls on 'unregister..' – Robert Rowntree Sep 11 '13 at 23:02
  • @RobertRowntree, yeah, I've tried that. My original fix for this issue was to try to reset the adapter data and call `notifyDatasetChanged()`. – John Leehey Sep 11 '13 at 23:40
  • did you try preserving the fragment (onretain = true) and then just redoing a fragmentTransaction after the hide/show? – Robert Rowntree Sep 12 '13 at 01:12
  • You have a `ViewPager` in another `ViewPager`? Did you use `getChildFragmentManager()` in the adapter of the `ViewPager`? – user Sep 12 '13 at 07:07
  • @RobertRowntree if you're talking about [setRetainInstance()](http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)), I'll try that now. – John Leehey Sep 12 '13 at 16:00
  • @Luksprog, there is only one viewpager. The viewpager is inside of a fragment that gets shown and hidden with fragment transactions inside of a frame. – John Leehey Sep 12 '13 at 16:01
  • that is the method i had in mind but, on 2nd thought, i am not sure it does anything on 'hide / show'. It comes into play on orientation changes and that is where my comment would be more relevant. – Robert Rowntree Sep 12 '13 at 16:02
  • i think the missing stack info is what are the delegated callbacks from the 'hide' fragment event that reach the ViewPager and that change the state of the pager. If u knew more about that (DEBUG=true) in the framework... then you would know more about how to approach the ViewPager – Robert Rowntree Sep 12 '13 at 16:07
  • @RobertRowntree but what would I do once those callbacks are triggered? Are you suggesting subclassing the viewpager and overriding the events? – John Leehey Sep 12 '13 at 17:23
  • http://grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/ see 'android/support/v4/view/viewpager for the source. step 1 u have the parent frag and state of view pager. step 2 you call 'hide' on the fragment parent . IMO this will delegate calls into Viewpager that result in state changes within ViewPager. If you knew more about those details, you would have better idea of how to tweek the state of the VP / Adapter to get your view restored properly. – Robert Rowntree Sep 12 '13 at 17:36
  • I think that you just loosing links for fragments, just try to reinit this – Lebedevsd Sep 18 '13 at 11:10
  • please post your FragmentStatePagerAdapter, as i am having similar structure in my app, and it works without any issues.. – Akhil Sep 18 '13 at 12:11
  • @Akhil I posted my pageradapter code. Looking at it again also gave me an idea, so I'll take a look now. – John Leehey Sep 18 '13 at 16:29
  • Scratch that, no dice. I tried forcing the use of the constructor that takes in MyInfos, just in case I debugged incorrectly. – John Leehey Sep 18 '13 at 16:42
  • Is `getFragment` called when you're recreating the activity? If so, is it non-null? Also, can you reveal us more code? Maybe relevants parts from parent activity and how you're using the sliding fragment. – gunar Sep 18 '13 at 19:50
  • @gunar, the fragment obtained in the viewpager does not become null, only the view for that fragment does. The hiding and the showing of the fragment use identical code to what you posted below. For the sliding fragment, I'll take a look and see what changes are made in my lifecycle methods that could cause the recreation of the view to fail. – John Leehey Sep 18 '13 at 20:33
  • In the sliding fragment are you using the FragmentManager of the activity or the child FragmentManager of the fragment to set the viewpager adapter? – gunar Sep 18 '13 at 20:39
  • @JohnLeehey The view of a fragment might need re-initialization at anytime. Maybe the error is in your implementation of the Fragment. On a side note, it might also be a good step to update your support library in case you're using it as there was an issue with the Child Fragment Manager. – Sherif elKhatib Sep 18 '13 at 20:53
  • @gunar I am using whatever is returned by getActivity().getSupportFragmentManager() in order to instantiate the pager adapter... should I be using a different fragment manager instance? – John Leehey Sep 18 '13 at 21:17
  • @SherifelKhatib, my support libraries are updated to revision 18, the latest. I'll check through my fragment code and post it above, but from what I can recall, all it does is override onCreateView, constructs a custom view, then returns it. I'll post it – John Leehey Sep 18 '13 at 21:20
  • 3
    When you're instantiating the viewpager adapter **from Fragment** pass the [getChildSupportFragmentManager](http://developer.android.com/reference/android/support/v4/app/Fragment.html#getChildFragmentManager()). The difference between these 2 is that this fragment manager is administrated by the fragment, not by host Activity. I am not sure if this would be *the thing*, but it would worth trying. – gunar Sep 18 '13 at 21:27
  • @gunar THAT WORKED! I did not know that method existed, but it makes total sense that the fragments stemmed off by the parent fragment would need their own separate fragment manager. Please incorporate that into your answer so I can accept, and you get the bounty of course! Thanks again for sticking with me. – John Leehey Sep 18 '13 at 21:36
  • I have that already in the provided answer. **PagerFragment#setupPagerData(...)** – gunar Sep 18 '13 at 21:37
  • augh, you are right. Do you mind editing it to draw attention to it? I'm sorry I overlooked it. I didn't know there was a separate way to get a fragment manager so I looked it over and mistook it for getSupportFragmentManager(). My apologies. – John Leehey Sep 18 '13 at 21:39

5 Answers5

96

You're not providing enough info for your specific issue, so I built a sample project that tries to reproduce your issue: the app has an activity that holds a fragment (PagerFragment) within a relative layout and below this layout I have a button that hides & shows above PagerFragment. PagerFragment has a ViewPager and each fragment within pager adapter simply displays a label - this fragment is named DataFragment. The label list is created in parent activity and passed to PagerFragment and then through its adapter to each DataFragment. Changing the PagerFragment visibility is done with no issues and each time it's becoming visible again it shows the previous shown label.

The key of the issue: Use Fragment#getChildFragmentManager() when you're creating the viewpager adapter and not getFragmentManager!

Maybe you can compare this simple project with what you have and check where are the differences. So here goes (top-down):

PagerActivity (the only activity in the project):

public class PagerActivity extends FragmentActivity {

    private static final String PAGER_TAG = "PagerActivity.PAGER_TAG";

    @Override
    protected void onCreate(Bundle savedInstance) {
        super.onCreate(savedInstance);
        setContentView(R.layout.pager_activity);
        if (savedInstance == null) {
            PagerFragment frag = PagerFragment.newInstance(buildPagerData());
            FragmentManager fm = getSupportFragmentManager();
            fm.beginTransaction().add(R.id.layout_fragments, frag, PAGER_TAG).commit();
        }
        findViewById(R.id.btnFragments).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                changeFragmentVisibility();
            }
        });
    }

    private List<String> buildPagerData() {
        ArrayList<String> pagerData = new ArrayList<String>();
        pagerData.add("Robert de Niro");
        pagerData.add("John Smith");
        pagerData.add("Valerie Irons");
        pagerData.add("Metallica");
        pagerData.add("Rammstein");
        pagerData.add("Zinedine Zidane");
        pagerData.add("Ronaldo da Lima");
        return pagerData;
    }

    protected void changeFragmentVisibility() {
        Fragment frag = getSupportFragmentManager().findFragmentByTag(PAGER_TAG);
        if (frag == null) {
            Toast.makeText(this, "No PAGER fragment found", Toast.LENGTH_SHORT).show();
            return;
        }
        boolean visible = frag.isVisible();
        Log.d("APSampler", "Pager fragment visibility: " + visible);
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        if (visible) {
            ft.hide(frag);
        } else {
            ft.show(frag);
        }
        ft.commit();
        getSupportFragmentManager().executePendingTransactions();
    }
}

its layout file pager_activity.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="4dp" >

    <Button
        android:id="@+id/btnFragments"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="Hide/Show fragments" />

    <RelativeLayout
        android:id="@+id/layout_fragments"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/btnFragments"
        android:layout_marginBottom="4dp" >
    </RelativeLayout>

</RelativeLayout>

Observe that I am adding the PagerFragment when the activity is first shown - and the PagerFragment class:

public class PagerFragment extends Fragment {

    private static final String DATA_ARGS_KEY = "PagerFragment.DATA_ARGS_KEY";

    private List<String> data;

    private ViewPager pagerData;

    public static PagerFragment newInstance(List<String> data) {
        PagerFragment pagerFragment = new PagerFragment();
        Bundle args = new Bundle();
        ArrayList<String> argsValue = new ArrayList<String>(data);
        args.putStringArrayList(DATA_ARGS_KEY, argsValue);
        pagerFragment.setArguments(args);
        return pagerFragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        data = getArguments().getStringArrayList(DATA_ARGS_KEY);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.pager_fragment, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        pagerData = (ViewPager) view.findViewById(R.id.pager_data);
        setupPagerData();
    }

    private void setupPagerData() {
        PagerAdapter adapter = new LocalPagerAdapter(getChildFragmentManager(), data);
        pagerData.setAdapter(adapter);
    }

}

its layout (only the ViewPager that takes full size):

<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager_data"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

and its adapter:

public class LocalPagerAdapter extends FragmentStatePagerAdapter {
    private List<String> pagerData;

    public LocalPagerAdapter(FragmentManager fm, List<String> pagerData) {
        super(fm);
        this.pagerData = pagerData;
    }

    @Override
    public Fragment getItem(int position) {
        return DataFragment.newInstance(pagerData.get(position));
    }

    @Override
    public int getCount() {
        return pagerData.size();
    }
}

This adapter creates a DataFragment for each page:

public class DataFragment extends Fragment {
    private static final String DATA_ARG_KEY = "DataFragment.DATA_ARG_KEY";

    private String localData;

    public static DataFragment newInstance(String data) {
        DataFragment df = new DataFragment();
        Bundle args = new Bundle();
        args.putString(DATA_ARG_KEY, data);
        df.setArguments(args);
        return df;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        localData = getArguments().getString(DATA_ARG_KEY);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.data_fragment, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        view.findViewById(R.id.btn_page_action).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Toast.makeText(getActivity(), localData, Toast.LENGTH_SHORT).show();
            }
        });
        ((TextView) view.findViewById(R.id.txt_label)).setText(localData);
    }
}

and DataFragment's layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="4dp" >

    <Button
        android:id="@+id/btn_page_action"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="Interogate" />

    <TextView
        android:id="@+id/txt_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>

Enjoy coding!

gunar
  • 14,660
  • 7
  • 56
  • 87
  • Hmmm the structure I have set up is very similar to what you are doing here. A small difference I can see is that the container you are using for the fragment views is a relative layout while I am using a FrameLayout. I'll compare side by side and see if there is anything else. – John Leehey Sep 17 '13 at 19:38
  • 3
    It shouldn't make any difference. – gunar Sep 17 '13 at 20:09
  • Did the comparison reveal anything relevant? – gunar Sep 18 '13 at 19:52
  • 8
    Just to follow up for those going straight to these comments, the comparison revealed (or should have, if I had looked more thoroughly) that I was using `getActivity().getSupportFragmentManager()` instead of `getChildFragmentManager()` in the creation of the pager adapter. – John Leehey Sep 18 '13 at 21:47
  • @gunar everything working fine, problem for me is for Toast.makeText(getActivity(), .....) is making error while it's not visible, I 'm using network calls for every child fragments I'm loading 7 child fragments for view-pager!! problem for me is when we swiping on the 4th fragment dude to network delay in 1st fragment will getting response which is not visible having Toast on response will causing error! – LOG_TAG Dec 09 '13 at 06:21
  • I guess `getActivity` is null and you're getting a `NullPointerException`. Try to initialize a Context in `onAttach` to application context and use that instead. – gunar Dec 09 '13 at 06:36
  • Thanks for this solution!, But the main issue with me is that I can't use childFragmentManger as it doesn't support API<17 and I have project requirement to keep min sdk 15. Is there anything else we can do? – Raj Suvariya May 04 '18 at 07:55
  • This exactly my case . Posted a question https://stackoverflow.com/questions/56079502/communication-between-parentfragment-and-fragment-in-viewpager . Please help – karthik kolanji May 11 '19 at 11:23
21

maybe it will help mViewPager.setOffscreenPageLimit(5)

Set the number of pages that should be retained to either side of the current page in the view hierarchy in an idle state. Pages beyond this limit will be recreated from the adapter when needed.

This is offered as an optimization. If you know in advance the number of pages you will need to support or have lazy-loading mechanisms in place on your pages, tweaking this setting can have benefits in perceived smoothness of paging animations and interaction. If you have a small number of pages (3-4) that you can keep active all at once, less time will be spent in layout for newly created view subtrees as the user pages back and forth.

You should keep this limit low, especially if your pages have complex layouts. This setting defaults to 1.

Kiryl Bielašeŭski
  • 2,663
  • 2
  • 28
  • 40
7

View Pager is pretty adamant in keeping keeping its Fragments fresh always and thus optimizing the performance by freeing up memory when a fragment is not used. Clearly that is a valid useful trait in a mobile system. But due to this persistent deallocation of resources the fragment is created everytime it gains focus.

mViewPager.setOffscreenPageLimit(NUMBEROFFRAGMENTSCREENS);

Here is the documentation.

this Old Post has an interesting Solution for your problem.. Please Refer

Community
  • 1
  • 1
Augustus Francis
  • 2,694
  • 4
  • 22
  • 32
6

For me i changed to getChildFragmentManager() instead of getFragmentManager() and works good. Ex:

pagerAdapt = new PagerAdapt(getChildFragmentManager());
Mahmoud Ayman
  • 1,157
  • 1
  • 17
  • 27
1

I had the same problem. My app (FragmentActivity) has a pager (ViewPager) with 3 framgents. While swiping between the fragments they are destroyed and recreated all the time. Actually it makes no problem in functionality (expect unclosed Cursors), but I was also wondering about this question.

I do not know if there is a workaround to change the behavior of the ViewPager, but I suggest to have a configuration object (maybe a static on) and before destroy save your myViewPager object at the config object.

public class App extends FragmentActivity {

    static MyData data;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
         data = (MyData) getLastCustomNonConfigurationInstance();
         if (data == null) {
             data = new MyData();
             data.savedViewPager = myViewPager;
         } else {
             myViewPager = data.savedViewPager;
        }
    }

    @Override
    public Object onRetainCustomNonConfigurationInstance() {
        Log.d("onRetainCustomNonConfigurationInstance", "Configuration call");
        return data;
    }
}

public class MyData {
    public ViewPager savedViewPager;
}

With this way, you can save the reference to the an object which won't be destroyed hence there is reference to it and you can reload all your crucial objects.

I hope you find my suggestion useful!

csikos.balint
  • 1,107
  • 2
  • 10
  • 25