I have an activity with swiping tabs using ActionBar tabs, based on the android developer example.
Each tab displays a Fragment, and each Fragment (actually, a SherlockFragment) loads a different kind of remote api request via a custom AsyncTaskLoader.
The problem is that if you tap a tab to move 2 tabs/pages over while the fragment for the tab you are leaving (the old fragment) is loading a result, that result is delivered to the fragment for the tab you move to (the new fragment). In my case, this leads to a ClassCastException, since the expected results are of incompatible types.
In code, the gist of the situation is:
Loaders:
public class FooLoader extends AsyncTaskLoader<Foo>
public class BarLoader extends AsyncTaskLoader<Bar>
Fragments:
public class FooFragment extends Fragment implements LoaderManager.LoaderCallbacks<Foo> {
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
public Loader<Foo> onCreateLoader(int id, Bundle args) { return new FooLoader(); }
...
}
public class BarFragment extends Fragment implements LoaderManager.LoaderCallbacks<Bar> {
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
public Loader<Bar> onCreateLoader(int id, Bundle args) { return new BarLoader(); }
...
}
The tab management code is as in the the aforementioned example. There is a third tab in between the Foo and Bar tabs (call it Baz). When we skip from the Foo tab to the Bar tab by tapping the Bar tab after FooFragment has called initLoader on its LoaderManager but before FooFragment.onLoadFinished is called, we end up with a ClassCastException on a call to BarFragment.onLoadFinished:
java.lang.ClassCastException: com.example.Foo cannot be cast to com.example.Bar
at com.example.BarFragment.onLoadFinished(BarFragment.java:1)
at android.support.v4.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:427)
at android.support.v4.app.LoaderManagerImpl.initLoader(LoaderManager.java:562)
at com.example.BarFragment.onCreate(BarFragment.java:36)
at android.support.v4.app.Fragment.performCreate(Fragment.java:1437)
...
Why is this happening, and how can it be prevented? It looks from the debug logs like the same LoaderManager is being re-used in the Bar fragment (though the Baz fragment has its own), but I don't know why that should happen.
Update: Using different loader IDs in each fragment does eliminate the crash (or seems to - I don't really know why) but I would rather not do this. In one of the fragments I actually create IDs dynamically and don't want to assume there will be no collision. Also, that solution is weird to me - loader IDs should be local to each fragment (otherwise, why can I have Loaders with the same IDs in different fragments under normal circumstances?)
It seems I can also eliminate the crash by calling setOffscreenPageLimit(2)
on my ViewPager, so that the Foo view is not discarded when we switch to the Bar view. But this is a workaround, not a general solution.
Full code: I have created an example application demonstrating the error. It includes a monkeyrunner script to force the error (though it may not work for all screen sizes).
Don't use 0 as your ID. As far as the LoaderManager
knows they're meant to be he same loader.
You can define unique IDs in an XML resource file
<item type="id" name="loader_foo" />
<item type="id" name="loader_bar" />
And access them from R.
loaderManager.initLoader(R.id.loader_foo, null, new LoaderCallbacks(){});
The documentation for LoaderManager
says "Identifiers are scoped to a particular LoaderManager instance". And instances of LoaderMananger
are tied to Activities
.
For generating unique IDs, you could manually assign them IDs that have large gaps vetween them.
private static final int LOADER_FOO = 1;
private static final int LOADER_BAR = 100;
for(int i = 0; i < 10; ++i){
loaderManager.initLoader(LOADER_FOO + i, null, new LoaderCallbacks(){});
}
You can avoid this issue by calling initLoader
in onActivityCreated
rather than in onCreate
- as noted by Alex Lockwood in the question comments. Modified code below.
Corrected Fragments:
public class FooFragment extends Fragment implements LoaderManager.LoaderCallbacks<Foo> {
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
public Loader<Foo> onCreateLoader(int id, Bundle args) { return new FooLoader(); }
...
}
public class BarFragment extends Fragment implements LoaderManager.LoaderCallbacks<Bar> {
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
public Loader<Bar> onCreateLoader(int id, Bundle args) { return new BarLoader(); }
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With