Loader delivers result to wrong fragment
K

3

13

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

Keavy answered 19/3, 2013 at 3:17 Comment(2)
Could you post the full code for one of the loaders please.Hydroelectric
Try moving your call to initLoader into onActivityCreated instead of onCreate.Bandoline
K
2

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(); }
    ...
}
Keavy answered 21/3, 2013 at 21:16 Comment(1)
I dont think this will work in case you have a viewpager with fragments of the same type. In that case the viewpager will destroy the fragment and re-create it once you swipe back, at that point the fragment will never receive a onActivityCreated event but only the onAttach(). IMHO you probably need ot make sure that the loader ids are different. If you are using the same fragment multiple times in the same activity you need to pass arguments to make sure that the loader ids dont collide.Susiesuslik
H
10

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(){});
}
Healey answered 19/3, 2013 at 5:38 Comment(4)
I can eliminate the crash by using different IDs in each fragment, but shouldn't have to do this and don't want to. (see update to original question).Keavy
The IDs I am generating don't have any particular bound, so there isn't a gap size I can assume would be large enough. Plus, that still feels like a workaround.Keavy
In that case, you could use a single loader and repeatedly call deliver result. As long as it's not the same object onLoadFinished will be called multiple times.Healey
Makes senses to me to use different IDs for different LoaderCallbacks on the same LoaderManager. Cleanest approach for my problem. ThanksMcmath
K
2

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(); }
    ...
}
Keavy answered 21/3, 2013 at 21:16 Comment(1)
I dont think this will work in case you have a viewpager with fragments of the same type. In that case the viewpager will destroy the fragment and re-create it once you swipe back, at that point the fragment will never receive a onActivityCreated event but only the onAttach(). IMHO you probably need ot make sure that the loader ids are different. If you are using the same fragment multiple times in the same activity you need to pass arguments to make sure that the loader ids dont collide.Susiesuslik
A
0

recently I was working with SimpleCursorAdapter and loaders for displaying data from sql lite tables. I spent a long time debugging an error "no such column '_id'." which occured in my 2nd fragment's onLoadFinished(). My first table had the '_id' column but my 2nd table didnt, which led me to believe that the Loader is delivering to the wrong fragment. So just in case you did something similar, ensure that your sql tables have the '_id' column first. Hope this helps anyone else who had the same issue as me.

Aligarh answered 29/4, 2014 at 17:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.