Question

This is the hierarchy of my app:

hierarchy

The 3 fragments inside the ViewPager contain a FrameLayout:

  • A loading spinner, visible while searching for data to fill the ListView.
  • A LinearLayout showing a message, visible when no data is found.
  • And a ListView.

The first and second fragments get their data from a ContentProvider, using a CursorLoader. And everything is working fine, except when the following situation occurs:

  1. Stop the app, pressing the home button, while on landscape mode.
  2. change to portrait mode, and start the app again.(actually, if I stay in landscape the error still exists, because when I resume the app, Android recreates the activity in portrait and then rotates it. But since i'm going to show you the LOG, let's avoid logging one extra lifecycle).

When the previous situation happens. The first and second fragment stay in the loadingSpinner, never show the listview. Let's see the Fragment1's code (the second fragment is pretty much the same):

public class FragmentOne extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

private LinearLayout emptyMsgContainer;
private ListView listView;
private ProgressBar loadingSpinner;
private Details mActivity;

FragmentOneListAdapter mAdapter;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("onCreateView()", "FragmentOne");
    View view = inflater.inflate(R.layout.fragment_one_list_fragment, container, false);
    emptyMsgContainer = (LinearLayout)view.findViewById(R.id.empty_message_container_1);
    listView = (ListView)view.findViewById(R.id.listView_1);
    loadingSpinner = (ProgressBar)view.findViewById(R.id.loading_spinner_1);

    return view;
}

@Override
public void onActivityCreated(Bundle icicle) {
    super.onActivityCreated(icicle);
    Log.d("onActivityCreated()", "FragmentOne");

    mAdapter = new FragmentOneListAdapter(getActivity(), null, 0);

    listView.setAdapter(mAdapter);
    listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    listView.setOnItemClickListener(mListListener);

    // executes initLoader and logs at the same time
    Log.d("onActivityCreated()", getActivity().getSupportLoaderManager().initLoader(1, null, this).toString());
}

@Override
public void onResume() {
    super.onResume();
    Log.d("onResume()", "FragmentOne");
    // used to communicate direclty with the MainActivity
    MainFragment parentFragment = (MainFragment)
            getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");
    mActivity = (Details)parentFragment.getDetailsListener();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri uri = ...;  
        String selection = "...";
        String[] selectionArgs = new String[] { ... };
        CursorLoader loader =  new CursorLoader(getActivity(), uri, null, selection, selectionArgs, null);
        Log.d("onCreateLoader()", loader.toString());
        return loader;
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            Log.d("onLoadFinished()", loader.toString());
    if(data.getCount() == 0) {
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);
        emptyMsgContainer.setVisibility(View.VISIBLE);  
    } else {
        mAdapter.swapCursor(data);
        myCursor = data;
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.VISIBLE);
        emptyMsgContainer.setVisibility(View.GONE);
    }
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
    myCursor = null;
}

private OnItemClickListener mListListener = new OnItemClickListener() {

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        mActivity.ShowStoredDetails(position, 1);
    }

};

}

I logged Activity and Fragment's lifecycle callbacks, onCreateLoader and onLoadFinished to try to figure out what is going on. First let's open the app in portrait:

23:04:00.089: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:04:00.139: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:04:00.139: D/onCreate()(8240): <!> ...  68<!> MainFragment
23:04:00.139: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:04:00.169: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:04:00.169: D/onStart()(8240): <!> ...  101<!> MainFragment
23:04:00.169: D/onStart()(8240): <!> ...  31<!> MainActivity
23:04:00.169: D/onResume()(8240): <!> ...  41<!> MainActivity
23:04:00.169: D/onResume()(8240): <!> ...  107<!> MainFragment
23:04:00.179: D/onattach()(8240): <!> ...  51<!> FragmentOne
23:04:00.189: D/onCreate()(8240): <!> ...  57<!> FragmentOne
23:04:00.189: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:04:00.199: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:04:00.199: D/onCreateLoader()(8240): <!> ...  146<!> CursorLoader{405c0e50 id=0}
23:04:00.209: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
23:04:00.209: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:04:00.209: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:04:00.219: D/onattach()(8240): <!> ...  50<!> FragmentTwo
23:04:00.219: D/onCreate()(8240): <!> ...  56<!> FragmentTwo
23:04:00.219: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:04:00.229: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:04:00.239: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:04:00.259: D/onResume()(8240): <!> ...  94<!> FragmentTwo
23:04:00.479: D/onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}

The Loader with id=1 doesn't exist, it is created. onCreateLoader() is called and after that onLoadFinished(). The ListView is filled and working fine. Now let's rotate to landscape:

23:04:26.999: D/onSaveInstanceState()(8240): <!> ...  113<!> MainFragment
23:04:27.009: D/onSaveInstanceState()(8240): <!> ...  105<!> FragmentOne
23:04:27.009: D/onSaveInstanceState()(8240): <!> ...  103<!> FragmentTwo
23:04:27.009: D/onPause()(8240): <!> ...  111<!> FragmentOne
23:04:27.009: D/onPause()(8240): <!> ...  109<!> FragmentTwo
23:04:27.009: D/onPause()(8240): <!> ...  119<!> MainFragment
23:04:27.009: D/onPause()(8240): <!> ...  46<!> MainActivity
23:04:27.019: D/onStop()(8240): <!> ...  117<!> FragmentOne
23:04:27.019: D/onStop()(8240): <!> ...  115<!> FragmentTwo
23:04:27.019: D/onStop()(8240): <!> ...  125<!> MainFragment
23:04:27.019: D/onStop()(8240): <!> ...  51<!> MainActivity
23:04:27.019: D/onDestroyView()(8240): <!> ...  123<!> FragmentOne
23:04:27.019: D/onDestroyView()(8240): <!> ...  121<!> FragmentTwo
23:04:27.029: D/onDestroyView()(8240): <!> ...  131<!> MainFragment
23:04:27.029: D/onDetach()(8240): <!> ...  143<!> MainFragment
23:04:27.039: D/onDestroy()(8240): <!> ...  56<!> MainActivity
23:04:27.069: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:04:27.079: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:04:27.169: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:04:27.199: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:04:27.199: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:04:27.209: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:04:27.209: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
23:04:27.219: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:04:27.239: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:04:27.239: D/onStart()(8240): <!> ...  101<!> MainFragment
23:04:27.239: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:04:27.249: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:04:27.249: D/onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}
23:04:27.249: D/onStart()(8240): <!> ...  31<!> MainActivity
23:04:27.259: D/onResume()(8240): <!> ...  41<!> MainActivity
23:04:27.259: D/onResume()(8240): <!> ...  107<!> MainFragment
23:04:27.259: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:04:27.259: D/onResume()(8240): <!> ...  94<!> FragmentTwo

The MainActivity is destroyed and also de view hierarchy of the Fragments, but the fragments instances remain the same (MainFragment uses setRetainInstance(true)). The MainActivity is recreated, the MainFragment attached to it, the FragmentOne's view hierarchy is created again and the ListView is filled with the same Loader id=1, it already exists so only onLoadFinished() is called. Now let's stop the app pressing the home button:

23:04:59.639: D/onSaveInstanceState()(8240): <!> ...  113<!> MainFragment
23:04:59.649: D/onSaveInstanceState()(8240): <!> ...  105<!> FragmentOne
23:04:59.649: D/onSaveInstanceState()(8240): <!> ...  103<!> FragmentTwo
23:04:59.649: D/onPause()(8240): <!> ...  111<!> FragmentOne
23:04:59.649: D/onPause()(8240): <!> ...  109<!> FragmentTwo
23:04:59.649: D/onPause()(8240): <!> ...  119<!> MainFragment
23:04:59.659: D/onPause()(8240): <!> ...  46<!> MainActivity
23:05:00.059: D/onStop()(8240): <!> ...  117<!> FragmentOne
23:05:00.069: D/onStop()(8240): <!> ...  115<!> FragmentTwo
23:05:00.069: D/onStop()(8240): <!> ...  125<!> MainFragment
23:05:00.069: D/onStop()(8240): <!> ...  51<!> MainActivity

Everything is stopped. Finally let's resume the app:

23:05:47.489: D/onDestroyView()(8240): <!> ...  123<!> FragmentOne
23:05:47.489: D/onDestroyView()(8240): <!> ...  121<!> FragmentTwo
23:05:47.499: D/onDestroyView()(8240): <!> ...  131<!> MainFragment
23:05:47.499: D/onDetach()(8240): <!> ...  143<!> MainFragment
23:05:47.499: D/onDestroy()(8240): <!> ...  56<!> MainActivity
23:05:47.509: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:05:47.509: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:05:47.539: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:05:47.569: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:05:47.569: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:05:47.579: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:05:47.579: D/onCreateLoader()(8240): <!> ...  146<!> CursorLoader{40540548 id=0}
23:05:47.579: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{40540548 id=0}
23:05:47.579: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:05:47.589: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:05:47.589: D/onStart()(8240): <!> ...  101<!> MainFragment
23:05:47.589: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:05:47.599: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:05:47.599: D/onStart()(8240): <!> ...  31<!> MainActivity
23:05:47.599: D/onResume()(8240): <!> ...  41<!> MainActivity
23:05:47.599: D/onResume()(8240): <!> ...  107<!> MainFragment
23:05:47.599: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:05:47.599: D/onResume()(8240): <!> ...  94<!> FragmentTwo

Activity and Fragments lifecycle completes. The Activity is recreated, the MainFragment attaches to it. FragmentOne's view hierarchy is created. But this time, the LoaderManager doesn't contain a Loader with id=1 anymore. When initLoader is executed, onCreateLoader() is called (the "id" parameter it receives is 1), but onLoadFinished() is not called and the loadingSpinner stays visible.

From the log you can compare the first time the app is executed (Loader id=1 doesn't exist)...

onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
onCreateLoader()(8240): <!> ...  146<!> CursorLoader{405c0e50 id=0}
onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}

...with the second time the loader id=1 doesn't exist:

onActivityCreated()(8240): <!> ...  83<!> CursorLoader{40540548 id=0}
onCreateLoader()(8240): <!> ...  146<!> CursorLoader{40540548 id=0}

The first time onActivityCreated (initLoader) returns a Loader with id=1, but the second time it returns id=0. I can only suppose that's the reason onLoadFinished() is not called the second time. As far as I know a LoaderManager is supposed to retain its state when orientation changes. Any ideas of what is happening here?

EDIT

I should have mentioned I'm using a support library:

import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
Was it helpful?

Solution

Ok, I've found similar cases: Loader restarts on orientation change.

And it seems to be a bug in the SupportLibrary, related with my implementation of nested Fragments.

To make it work, I had to change the location of the LoaderCallbacks interface and the initLoader(), from FragmentOne and FragmentTwo to the MainActivity. It's a little bit messy, because i had to create some interfaces, but it does the work.

I'll explain in case someone finds himself in this situation:

First, I created two interfaces:

ListenerFragments interface, is implemented in the MainActivity and is used from FragmentOne and FragmentTwo to register themselves in the MainActivity as fragments that are going to be using loaders:

public interface ListenerFragments {
    public void setFragmentOne(FragmentsUICallbacks callbacks);
    public void setFragmentTwo(FragmentsUICallbacks callbacks);
    public void prepareLoader(int id);
}

The second interface, is implemented in FragmentOne and FragmentTwo. And consist of methods that are going to change the Fragment's UI, swapping the cursor and making the FrameLayout childs (ListView, LoadingSpinner...) visible or not. Also, this is the interface we are going to be passing to the MainActivity's setFragmentOne() and setFragmentTwo(), so it can modify the UI when onLoadFinished() and onLoaderReset() are called:

public interface FragmentsUICallbacks {
        public void emptyCursor();
        public void assignCursor(Cursor data);
    public void clearCursorReferences();
}

The MainActivity is implementing ListenerFragments and LoaderCallbacks<Cursor> interfaces:

public class MainActivity extends ActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>, ListenerFragments {
    private FragmentsUICallbacks fragmentOneCallbacks;
    private FragmentsUICallbacks fragmentTwoCallbacks;

    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri uri;
        String selection;
        String[] selectionArgs;
        switch(id) {
            case 1:
                uri = ...;  
                selection = "...";
                selectionArgs = new String[] { ... };
                return new CursorLoader(this, uri, null, selection, selectionArgs, null);
            case 2:
                ...
        }
        return null;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        switch(loader.getId()) {
            case 1:
                if(data.getCount() == 0) {
                    fragmentOneCallbacks.emptyCursor(); 
                } else {
                    fragmentOneCallbacks.assignCursor(data);
                }
                break;
            case 2:
                ...
            }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        switch(loader.getId()) {
            case 1:
                fragmentOneCallbacks.clearCursorReferences();
                break;
            case 2:
                ...
        }
    }

    @Override
    public void setFragmentOne(FragmentsUICallbacks callbacks) {
        if(callbacks != null)
            this.fragmentOneCallbacks = callbacks;
    }

    @Override
    public void setFragmentTwo(FragmentsUICallbacks callbacks) {
        if(callbacks != null)
            this.fragmentTwoCallbacks = callbacks;
    }

    @Override
    public void prepareLoader(int id) {
        getSupportLoaderManager().initLoader(id, null, this);
    }
}

The code is pretty straightforward. The tricky part comes in FragmentOne's onResume():

public class FragmentOne extends Fragment implements FragmentsUICallbacks {

    ...

    @Override
    public void onResume() {
        super.onResume();
        MainFragment parentFragment = (MainFragment)
            getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");

        ListenerFragments listenerFragments = (ListenerFragments)parentFragment.getListenerFragments();
        listenerFragments.setFragmentOne(this);
        listenerFragments.prepareLoader(1);
    }

    public void emptyCursor() {
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);
        emptyMsgContainer.setVisibility(View.VISIBLE);  
    }

    public void assignCursor(Cursor data) {
        mAdapter.swapCursor(data);
        myCursor = data;
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.VISIBLE);
        emptyMsgContainer.setVisibility(View.GONE);
    }

    public void clearCursorReferences() {
        mAdapter.swapCursor(null);
        myCursor = null;
    }

}

We need to get a reference to the ListenerFragment interface's methods the MainActivity is implementing, in order to inform it FragmentOne is going to be starting a loader. We get that reference through the MainFragment, Why? because we can't get it directly from FragmentOne.onAttach(Activity activity), since it is only called the first time the app is started, and the fragment is neither destroyed nor detached, when orientation changes the fragment goes from onDestroyView() to onCreateView(). onAttach() is not called.

On the other hand, MainFragment, is not destroyed either (setRetainInstance(true)), but it is detached from the old MainActivity and attached again to the new MainActivity when orientation change completes. We use onAttach() to hold the reference and we create a getter method so the fragments inside the ViewPager can get that reference:

public class MainFragment extends Fragment implements OnClickListener {

    private ListenerFragments listenerFragments;

    @Override
    public void onAttach(Activity myActivity) {
        super.onAttach(myActivity);
        this.listenerFragments = (ListenerFragments)myActivity;
    }

    public ListenerFragments getListenerFragments() {
            return listenerFragments;
    }

}

Knowing that, we can get back to FragmentOne.onResume(), where we get a reference to the MainFragment:

MainFragment parentFragment = (MainFragment)
    getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");

We use the MainFragment getter method we created to get the get access to the MainActivity methods:

    ListenerFragments listenerFragments = (ListenerFragments)parentFragment.getListenerFragments();
    listenerFragments.setFragmentOne(this);
    listenerFragments.prepareLoader(1);

and that's basically it.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top