Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android: CursorLoader crash on non-topmost Fragment

I have a few ListFragments that use a CursorLoader to retrieve their content. As the user drills down through the content, one Fragment replaces another (the Activity remains the same). But if the content changes on a non-topmost Fragment, the app crashes:

E/AndroidRuntime(18830): FATAL EXCEPTION: main
E/AndroidRuntime(18830): java.lang.RuntimeException: Unable to resume activity {com.example.ExampleApp/com.example.ExampleApp.ExampleActivity}: java.lang.IllegalStateException: Content view not yet created
E/AndroidRuntime(18830):        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2120)
E/AndroidRuntime(18830):        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2135)
E/AndroidRuntime(18830):        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:957)
E/AndroidRuntime(18830):        at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(18830):        at android.os.Looper.loop(Looper.java:130)
E/AndroidRuntime(18830):        at android.app.ActivityThread.main(ActivityThread.java:3683)
E/AndroidRuntime(18830):        at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(18830):        at java.lang.reflect.Method.invoke(Method.java:507)
E/AndroidRuntime(18830):        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
E/AndroidRuntime(18830):        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
E/AndroidRuntime(18830):        at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime(18830): Caused by: java.lang.IllegalStateException: Content view not yet created
E/AndroidRuntime(18830):        at android.support.v4.app.ListFragment.ensureList(ListFragment.java:328)
E/AndroidRuntime(18830):        at android.support.v4.app.ListFragment.setListShown(ListFragment.java:280)
E/AndroidRuntime(18830):        at android.support.v4.app.ListFragment.setListShownNoAnimation(ListFragment.java:266)
E/AndroidRuntime(18830):        at com.example.ExampleApp.FirstListFragment.onLoadFinished(FirstListFragment.java:102)
E/AndroidRuntime(18830):        at com.example.ExampleApp.FristListFragment.onLoadFinished(FirstListFragment.java:20)
E/AndroidRuntime(18830):        at android.support.v4.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:414)
E/AndroidRuntime(18830):        at android.support.v4.app.LoaderManagerImpl$LoaderInfo.reportStart(LoaderManager.java:298)
E/AndroidRuntime(18830):        at android.support.v4.app.LoaderManagerImpl.doReportStart(LoaderManager.java:751)
E/AndroidRuntime(18830):        at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:512)
E/AndroidRuntime(18830):        at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1129)
E/AndroidRuntime(18830):        at android.app.Activity.performStart(Activity.java:3791)
E/AndroidRuntime(18830):        at android.app.Activity.performRestart(Activity.java:3821)
E/AndroidRuntime(18830):        at android.app.Activity.performResume(Activity.java:3826)
E/AndroidRuntime(18830):        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2110)
E/AndroidRuntime(18830):        ... 10 more

This can happen when a background update changes the content, or when the user returns from the home screen and another Fragment is at the top of the back stack. The onLoadFinished handler tries to update the UI, which no longer exists. But why is the fragment still getting cursor updates anyway?

I've worked around it by aborting the update if the fragment is not visible, but this seems like the wrong thing to do.

Here's what my Fragment looks like, edited a little for brevity:

public class FirstListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Initialize empty cursor adapter.
        mAdapter = new ExampleCursorAdapter(getActivity(), null, 0);
        setListAdapter(mAdapter);

        // Start with a progress indicator
        setListShown(false);

        // Prepare the loader.  Either re-connect with an existing one, or start a new one.
        getLoaderManager().initLoader(EXAMPLE_LOADER_ID, null, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), ExampleProvider.CONTENT_URI, PROJECTION, null, null, null);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        // Do nothing if we're not visible.
        if(!isVisible()) {
            return;
        }

        // Swap the new cursor in.  (The framework will take care of closing the
        // old cursor once we return.)
        mAdapter.swapCursor(data);

        // The list should now be shown.
        if(isResumed()) {
            setListShown(true);
        } else {
            setListShownNoAnimation(true);
        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        // This is called when the last Cursor provided to onLoadFinished() above is about to be
        // closed.  We need to make sure we are no longer using it.
        mAdapter.swapCursor(null);
    }
}

The isVisible() check prevents the crash, but it's not in any of the example code I've seen. What's wrong here?


Edit: Sure enough, I wound up with a StaleDataException with an old view trying to use a closed cursor. So now I'm destroying the LoaderManager when the view is destroyed. Still not sure if this is the right thing to do, and I can't reproduce the StaleDataException.

I removed the isVisible() hack from above and added this:

@Override
public void onDestroyView() {
    super.onDestroyView();

    // The CursorLoader example doesn't do this, but if we get an update while the UI is
    // destroyed, it will crash.  Why is this necessary?
    getLoaderManager().destroyLoader(EXAMPLE_LOADER_ID);
}
like image 646
kvance Avatar asked Jul 20 '11 04:07

kvance


1 Answers

This seems to be a problem with list fragments getting the list view. For some reason getListView() will not return a valid view unless the fragment is showing. I've seen it throw an illegal state exception on anything in the life cycle before onStart().

The way that I solve the problem is I call the loader in the onCreateView() method. When the loader returns with data I check to see if the activity is started. If it is then I load it into the list view, if not I store it in a local variable, and then load the data in the onStart() method.

This seems like a super hack, but it does work.

like image 94
Brian Griffey Avatar answered Oct 31 '22 21:10

Brian Griffey