This is the hierarchy of my app:
The 3 fragments inside the ViewPager contain a FrameLayout:
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:
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;
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.