Android: CursorLoader сбой на не верхнем фрагменте

У меня есть несколько ListFragments, которые используют CursorLoader для извлечения их содержимого. По мере того, как пользователь перебирает содержимое, один фрагмент заменяет другой (действие остается тем же). Но если содержимое изменяется на не-верхнем фрагменте, приложение вылетает:

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

Это может произойти, когда фоновое обновление изменяет содержимое или когда пользователь возвращается с главного экрана, а другой фрагмент находится в верхней части заднего стека. Обработчик onLoadFinished пытается обновить пользовательский интерфейс, который больше не существует. Но почему фрагмент все равно получает обновления курсора?

Я работал над этим, прервав обновление, если фрагмент не виден, но это кажется неправильным.

Вот как выглядит мой фрагмент, немного отредактированный для краткости:

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);
    }
}

Проверка isVisible() предотвращает крах, но это не в каком-либо примере кода, который я видел. Что здесь не так?


Изменить: Конечно, я закончил с исключением StaleDataException со старым представлением, пытающимся использовать закрытый курсор. Итак, теперь я уничтожаю LoaderManager, когда представление уничтожено. Все еще не уверен, что это правильно, и я не могу воспроизвести исключение StaleDataException.

Я удалил снимок isVisible() сверху и добавил:

@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);
}

Ответы

Ответ 1

Это, кажется, проблема с фрагментами списка, получающими представление списка. По какой-то причине getListView() не вернет действительный вид, если только фрагмент не показывается. Я видел, как он выкидывал незаконное государственное исключение на что-либо в жизненном цикле перед onStart().

Как я решаю проблему, я вызываю загрузчик в методе onCreateView(). Когда загрузчик возвращается с данными, я проверяю, запускается ли действие. Если это так, я загружаю его в представление списка, если не храню его в локальной переменной, а затем загружаю данные в метод onStart().

Это похоже на супер хак, но он действительно работает.