Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loader delivers result to wrong fragment

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

like image 799
jgiles Avatar asked Mar 19 '13 03:03

jgiles


2 Answers

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(){});
}
like image 62
alex Avatar answered Nov 10 '22 03:11

alex


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(); }
    ...
}
like image 29
jgiles Avatar answered Nov 10 '22 04:11

jgiles