Погрузчик поставляет результат в неправильный фрагмент

У меня есть активность с прокручиванием вкладок с помощью вкладок ActionBar на основе примера разработчика Android.

Каждая вкладка отображает фрагмент, и каждый фрагмент (на самом деле, Шерлок-Фрагмент) загружает другой вид удаленного запроса api через пользовательский AsyncTaskLoader.

Проблема заключается в том, что если вы нажмете вкладку, чтобы переместить 2 вкладки/страницы, а фрагмент для вкладки, которую вы оставляете (старый фрагмент), загружает результат, этот результат доставляется к фрагменту для вкладки, которую вы перемещаете к (новый фрагмент). В моем случае это приводит к исключению ClassCastException, поскольку ожидаемые результаты имеют несовместимые типы.

В коде суть сущности:

Погрузчики:

public class FooLoader extends AsyncTaskLoader<Foo>
public class BarLoader extends AsyncTaskLoader<Bar>

Фрагменты:

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

Код управления вкладками приведен в вышеупомянутом примере. Между вкладками Foo и Bar есть третья вкладка (назовите ее Baz). Когда мы переходим с вкладки Foo на вкладку "Бар", нажимая на вкладку "Бар" после того, как FooFragment вызвал initLoader в своем LoaderManager, но до того, как вызывается FooFragment.onLoadFinished, мы заканчиваем ClassCastException при вызове 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)
...

Почему это происходит, и как его можно предотвратить? Это выглядит из журналов отладки, так как тот же LoaderManager повторно используется в фрагменте Bar (хотя у Baz-фрагмента есть свой собственный), но я не знаю, почему это должно произойти.

Обновление: Использование разных идентификаторов загрузчика в каждом фрагменте устраняет крах (или, кажется, - я действительно не знаю почему), но я бы предпочел не делать этого. В одном из фрагментов я фактически создаю идентификаторы динамически и не хочу предполагать, что столкновения не будет. Кроме того, это решение странно для меня - идентификаторы загрузчика должны быть локальными для каждого фрагмента (в противном случае, почему я могу иметь загрузчики с одинаковыми идентификаторами в разных фрагментах при обычных обстоятельствах?)

Кажется, я также могу устранить крах, вызывая setOffscreenPageLimit(2) на моем ViewPager, так что просмотр Foo не отбрасывается, когда мы переключаемся на представление Bar. Но это обходное решение, а не общее решение.

Полный код: Я создал пример приложения демонстрирующий ошибку. Он включает в себя monkeyrunner script для принудительной ошибки (хотя он может не работать для всех размеров экрана).

Ответ 1

Вы можете избежать этой проблемы, вызвав initLoader в onActivityCreated, а не в onCreate - как отмечено Алексом Локвудом в комментариях к вопросу. Измененный код ниже.

Исправленные фрагменты:

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

Ответ 2

Не используйте 0 в качестве своего идентификатора. Поскольку LoaderManager знает, что они предназначены для того же самого загрузчика.

Вы можете определить уникальные идентификаторы в файле ресурсов XML

<item type="id" name="loader_foo" />
<item type="id" name="loader_bar" />

И получить их от R.

loaderManager.initLoader(R.id.loader_foo, null, new LoaderCallbacks(){});

В документации для LoaderManager говорится: "Идентификаторы привязаны к определенному экземпляру LoaderManager". И экземпляры LoaderMananger привязаны к Activities.

Для генерации уникальных идентификаторов вы можете вручную назначить им идентификаторы с большими пробелами между ними.

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

Ответ 3

Недавно я работал с SimpleCursorAdapter и загрузчиками для отображения данных из таблиц sql lite. Я долгое время отлаживал ошибку "нет такого столбца" _id ". который произошел в моем втором фрагменте onLoadFinished(). В моей первой таблице был столбец" _id ", но моя вторая таблица did not, что заставило меня поверить, что загрузчик доставляет неправильный фрагмент. Поэтому на всякий случай вы сделали что-то подобное, убедитесь, что ваши таблицы sql сначала имеют столбец" _id". Надеюсь, это поможет любому, у кого была такая же проблема, как и я.