Поведение LoaderManager при изменении ориентации

Это иерархия моего приложения:

иерархия

3 фрагмента внутри ViewPager содержат FrameLayout:

  • Счетчик загрузки, видимый при поиске данных для заполнения ListView.
  • LinearLayout, показывающий сообщение, видимое, когда данные не найдены.
  • И ListView.

Первый и второй фрагменты получают свои данные от ContentProvider, используя CursorLoader. И все работает нормально, за исключением случаев, когда возникает следующая ситуация:

  1. Остановите приложение, нажав кнопку «Домой» в ландшафтном режиме.
  2. переключитесь в портретный режим и снова запустите приложение (на самом деле, если я останусь в альбомной ориентации, ошибка все еще существует, потому что, когда я возобновляю работу приложения, Android воссоздает действие в портретном режиме, а затем поворачивает его. Но поскольку я собираюсь показать вам LOG, давайте не будем регистрировать один дополнительный жизненный цикл).

Когда произойдет предыдущая ситуация. Первый и второй фрагмент остаются в loadSpinner, никогда не показывают список. Давайте посмотрим код Fragment1 (второй фрагмент почти такой же):

public class FragmentOne extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

private LinearLayout emptyMsgContainer;
private ListView listView;
private ProgressBar loadingSpinner;
private Details mActivity;

FragmentOneListAdapter mAdapter;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("onCreateView()", "FragmentOne");
    View view = inflater.inflate(R.layout.fragment_one_list_fragment, container, false);
    emptyMsgContainer = (LinearLayout)view.findViewById(R.id.empty_message_container_1);
    listView = (ListView)view.findViewById(R.id.listView_1);
    loadingSpinner = (ProgressBar)view.findViewById(R.id.loading_spinner_1);

    return view;
}

@Override
public void onActivityCreated(Bundle icicle) {
    super.onActivityCreated(icicle);
    Log.d("onActivityCreated()", "FragmentOne");

    mAdapter = new FragmentOneListAdapter(getActivity(), null, 0);

    listView.setAdapter(mAdapter);
    listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    listView.setOnItemClickListener(mListListener);

    // executes initLoader and logs at the same time
    Log.d("onActivityCreated()", getActivity().getSupportLoaderManager().initLoader(1, null, this).toString());
}

@Override
public void onResume() {
    super.onResume();
    Log.d("onResume()", "FragmentOne");
    // used to communicate direclty with the MainActivity
    MainFragment parentFragment = (MainFragment)
            getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");
    mActivity = (Details)parentFragment.getDetailsListener();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri uri = ...;  
        String selection = "...";
        String[] selectionArgs = new String[] { ... };
        CursorLoader loader =  new CursorLoader(getActivity(), uri, null, selection, selectionArgs, null);
        Log.d("onCreateLoader()", loader.toString());
        return loader;
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            Log.d("onLoadFinished()", loader.toString());
    if(data.getCount() == 0) {
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);
        emptyMsgContainer.setVisibility(View.VISIBLE);  
    } else {
        mAdapter.swapCursor(data);
        myCursor = data;
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.VISIBLE);
        emptyMsgContainer.setVisibility(View.GONE);
    }
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
    myCursor = null;
}

private OnItemClickListener mListListener = new OnItemClickListener() {

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        mActivity.ShowStoredDetails(position, 1);
    }

};

}

Я зарегистрировал обратные вызовы жизненного цикла Activity и Fragment, onCreateLoader и onLoadFinished, чтобы попытаться выяснить, что происходит. Сначала давайте откроем приложение в портретной ориентации:

23:04:00.089: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:04:00.139: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:04:00.139: D/onCreate()(8240): <!> ...  68<!> MainFragment
23:04:00.139: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:04:00.169: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:04:00.169: D/onStart()(8240): <!> ...  101<!> MainFragment
23:04:00.169: D/onStart()(8240): <!> ...  31<!> MainActivity
23:04:00.169: D/onResume()(8240): <!> ...  41<!> MainActivity
23:04:00.169: D/onResume()(8240): <!> ...  107<!> MainFragment
23:04:00.179: D/onattach()(8240): <!> ...  51<!> FragmentOne
23:04:00.189: D/onCreate()(8240): <!> ...  57<!> FragmentOne
23:04:00.189: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:04:00.199: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:04:00.199: D/onCreateLoader()(8240): <!> ...  146<!> CursorLoader{405c0e50 id=0}
23:04:00.209: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
23:04:00.209: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:04:00.209: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:04:00.219: D/onattach()(8240): <!> ...  50<!> FragmentTwo
23:04:00.219: D/onCreate()(8240): <!> ...  56<!> FragmentTwo
23:04:00.219: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:04:00.229: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:04:00.239: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:04:00.259: D/onResume()(8240): <!> ...  94<!> FragmentTwo
23:04:00.479: D/onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}

Загрузчик с id=1 не существует, он создан. вызывается onCreateLoader() и после этого onLoadFinished(). ListView заполнен и работает нормально. Теперь давайте повернем в ландшафт:

23:04:26.999: D/onSaveInstanceState()(8240): <!> ...  113<!> MainFragment
23:04:27.009: D/onSaveInstanceState()(8240): <!> ...  105<!> FragmentOne
23:04:27.009: D/onSaveInstanceState()(8240): <!> ...  103<!> FragmentTwo
23:04:27.009: D/onPause()(8240): <!> ...  111<!> FragmentOne
23:04:27.009: D/onPause()(8240): <!> ...  109<!> FragmentTwo
23:04:27.009: D/onPause()(8240): <!> ...  119<!> MainFragment
23:04:27.009: D/onPause()(8240): <!> ...  46<!> MainActivity
23:04:27.019: D/onStop()(8240): <!> ...  117<!> FragmentOne
23:04:27.019: D/onStop()(8240): <!> ...  115<!> FragmentTwo
23:04:27.019: D/onStop()(8240): <!> ...  125<!> MainFragment
23:04:27.019: D/onStop()(8240): <!> ...  51<!> MainActivity
23:04:27.019: D/onDestroyView()(8240): <!> ...  123<!> FragmentOne
23:04:27.019: D/onDestroyView()(8240): <!> ...  121<!> FragmentTwo
23:04:27.029: D/onDestroyView()(8240): <!> ...  131<!> MainFragment
23:04:27.029: D/onDetach()(8240): <!> ...  143<!> MainFragment
23:04:27.039: D/onDestroy()(8240): <!> ...  56<!> MainActivity
23:04:27.069: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:04:27.079: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:04:27.169: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:04:27.199: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:04:27.199: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:04:27.209: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:04:27.209: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
23:04:27.219: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:04:27.239: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:04:27.239: D/onStart()(8240): <!> ...  101<!> MainFragment
23:04:27.239: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:04:27.249: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:04:27.249: D/onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}
23:04:27.249: D/onStart()(8240): <!> ...  31<!> MainActivity
23:04:27.259: D/onResume()(8240): <!> ...  41<!> MainActivity
23:04:27.259: D/onResume()(8240): <!> ...  107<!> MainFragment
23:04:27.259: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:04:27.259: D/onResume()(8240): <!> ...  94<!> FragmentTwo

MainActivity уничтожается, а также просматривается иерархия фрагментов, но экземпляры фрагментов остаются прежними (MainFragment использует setRetainInstance(true)). Активность MainActivity воссоздается, к ней прикрепляется MainFragment, снова создается иерархия представлений FragmentOne, а ListView заполняется тем же идентификатором загрузчика = 1, он уже существует, поэтому вызывается только onLoadFinished(). Теперь давайте остановим приложение, нажав кнопку «Домой»:

23:04:59.639: D/onSaveInstanceState()(8240): <!> ...  113<!> MainFragment
23:04:59.649: D/onSaveInstanceState()(8240): <!> ...  105<!> FragmentOne
23:04:59.649: D/onSaveInstanceState()(8240): <!> ...  103<!> FragmentTwo
23:04:59.649: D/onPause()(8240): <!> ...  111<!> FragmentOne
23:04:59.649: D/onPause()(8240): <!> ...  109<!> FragmentTwo
23:04:59.649: D/onPause()(8240): <!> ...  119<!> MainFragment
23:04:59.659: D/onPause()(8240): <!> ...  46<!> MainActivity
23:05:00.059: D/onStop()(8240): <!> ...  117<!> FragmentOne
23:05:00.069: D/onStop()(8240): <!> ...  115<!> FragmentTwo
23:05:00.069: D/onStop()(8240): <!> ...  125<!> MainFragment
23:05:00.069: D/onStop()(8240): <!> ...  51<!> MainActivity

Все остановлено. Наконец, давайте возобновим работу приложения:

23:05:47.489: D/onDestroyView()(8240): <!> ...  123<!> FragmentOne
23:05:47.489: D/onDestroyView()(8240): <!> ...  121<!> FragmentTwo
23:05:47.499: D/onDestroyView()(8240): <!> ...  131<!> MainFragment
23:05:47.499: D/onDetach()(8240): <!> ...  143<!> MainFragment
23:05:47.499: D/onDestroy()(8240): <!> ...  56<!> MainActivity
23:05:47.509: D/onAttach()(8240): <!> ...  61<!> MainFragment
23:05:47.509: D/onCreate()(8240): <!> ...  21<!> MainActivity
23:05:47.539: D/onCreateView()(8240): <!> ...  75<!> MainFragment
23:05:47.569: D/onActivityCreated()(8240): <!> ...  95<!> MainFragment
23:05:47.569: D/onCreateView()(8240): <!> ...  62<!> FragmentOne
23:05:47.579: D/onActivityCreated()(8240): <!> ...  74<!> FragmentOne
23:05:47.579: D/onCreateLoader()(8240): <!> ...  146<!> CursorLoader{40540548 id=0}
23:05:47.579: D/onActivityCreated()(8240): <!> ...  83<!> CursorLoader{40540548 id=0}
23:05:47.579: D/onCreateView()(8240): <!> ...  61<!> FragmentTwo
23:05:47.589: D/onActivityCreated()(8240): <!> ...  73<!> FragmentTwo
23:05:47.589: D/onStart()(8240): <!> ...  101<!> MainFragment
23:05:47.589: D/onStart()(8240): <!> ...  90<!> FragmentOne
23:05:47.599: D/onStart()(8240): <!> ...  88<!> FragmentTwo
23:05:47.599: D/onStart()(8240): <!> ...  31<!> MainActivity
23:05:47.599: D/onResume()(8240): <!> ...  41<!> MainActivity
23:05:47.599: D/onResume()(8240): <!> ...  107<!> MainFragment
23:05:47.599: D/onResume()(8240): <!> ...  96<!> FragmentOne
23:05:47.599: D/onResume()(8240): <!> ...  94<!> FragmentTwo

Жизненный цикл активности и фрагментов завершен. Активность создается заново, к ней присоединяется MainFragment. Иерархия представлений FragmentOne создана. Но на этот раз LoaderManager больше не содержит загрузчика с id=1. Когда initLoader выполняется, вызывается onCreateLoader() (параметр "id", который он получает, равен 1), но onLoadFinished() не вызывается, и загрузкаSpinner остается видимой.

Из журнала вы можете сравнить первый запуск приложения (идентификатор загрузчика = 1 не существует)...

onActivityCreated()(8240): <!> ...  83<!> CursorLoader{405c0e50 id=1}
onCreateLoader()(8240): <!> ...  146<!> CursorLoader{405c0e50 id=0}
onLoadFinished()(8240): <!> ...  153<!> CursorLoader{405c0e50 id=1}

... во второй раз идентификатор загрузчика = 1 не существует:

onActivityCreated()(8240): <!> ...  83<!> CursorLoader{40540548 id=0}
onCreateLoader()(8240): <!> ...  146<!> CursorLoader{40540548 id=0}

В первый раз onActivityCreated (initLoader) возвращает загрузчик с id=1, а во второй раз возвращает id=0. Я могу только предположить, что по этой причине onLoadFinished() не вызывается во второй раз. Насколько я знаю, LoaderManager должен сохранять свое состояние при изменении ориентации. Любые идеи о том, что здесь происходит?

ИЗМЕНИТЬ

Я должен был упомянуть, что использую библиотеку поддержки:

import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;

person ILovemyPoncho    schedule 25.04.2014    source источник


Ответы (1)


Хорошо, я нашел похожие случаи: Загрузчик перезапускается при изменении ориентации.

И похоже, это ошибка в SupportLibrary, связанная с моей реализацией вложенных фрагментов.

Чтобы заставить его работать, мне пришлось изменить расположение интерфейса LoaderCallbacks и initLoader() с FragmentOne и FragmentTwo на MainActivity. Это немного запутанно, потому что мне пришлось создать несколько интерфейсов, но это работает.

Поясню на случай, если кто-то окажется в такой ситуации:

Сначала я создал два интерфейса:

ListenerFragments реализован в MainActivity и используется из FragmentOne и FragmentTwo для регистрации себя в MainActivity как фрагменты, которые будут использовать загрузчики:

public interface ListenerFragments {
    public void setFragmentOne(FragmentsUICallbacks callbacks);
    public void setFragmentTwo(FragmentsUICallbacks callbacks);
    public void prepareLoader(int id);
}

Второй интерфейс реализован в FragmentOne и FragmentTwo. И состоят из методов, которые собираются изменить пользовательский интерфейс фрагмента, поменять местами курсор и сделать FrameLayout дочерних элементов (ListView, LoadingSpinner...) видимыми или нет. Кроме того, это интерфейс, который мы собираемся передать setFragmentOne() и setFragmentTwo() MainActivity, чтобы он мог изменять пользовательский интерфейс при вызове onLoadFinished() и onLoaderReset():

public interface FragmentsUICallbacks {
        public void emptyCursor();
        public void assignCursor(Cursor data);
    public void clearCursorReferences();
}

MainActivity реализует интерфейсы ListenerFragments и LoaderCallbacks<Cursor>:

public class MainActivity extends ActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>, ListenerFragments {
    private FragmentsUICallbacks fragmentOneCallbacks;
    private FragmentsUICallbacks fragmentTwoCallbacks;

    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Uri uri;
        String selection;
        String[] selectionArgs;
        switch(id) {
            case 1:
                uri = ...;  
                selection = "...";
                selectionArgs = new String[] { ... };
                return new CursorLoader(this, uri, null, selection, selectionArgs, null);
            case 2:
                ...
        }
        return null;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        switch(loader.getId()) {
            case 1:
                if(data.getCount() == 0) {
                    fragmentOneCallbacks.emptyCursor(); 
                } else {
                    fragmentOneCallbacks.assignCursor(data);
                }
                break;
            case 2:
                ...
            }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        switch(loader.getId()) {
            case 1:
                fragmentOneCallbacks.clearCursorReferences();
                break;
            case 2:
                ...
        }
    }

    @Override
    public void setFragmentOne(FragmentsUICallbacks callbacks) {
        if(callbacks != null)
            this.fragmentOneCallbacks = callbacks;
    }

    @Override
    public void setFragmentTwo(FragmentsUICallbacks callbacks) {
        if(callbacks != null)
            this.fragmentTwoCallbacks = callbacks;
    }

    @Override
    public void prepareLoader(int id) {
        getSupportLoaderManager().initLoader(id, null, this);
    }
}

Код довольно прост. Сложная часть начинается с onResume() FragmentOne:

public class FragmentOne extends Fragment implements FragmentsUICallbacks {

    ...

    @Override
    public void onResume() {
        super.onResume();
        MainFragment parentFragment = (MainFragment)
            getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");

        ListenerFragments listenerFragments = (ListenerFragments)parentFragment.getListenerFragments();
        listenerFragments.setFragmentOne(this);
        listenerFragments.prepareLoader(1);
    }

    public void emptyCursor() {
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.GONE);
        emptyMsgContainer.setVisibility(View.VISIBLE);  
    }

    public void assignCursor(Cursor data) {
        mAdapter.swapCursor(data);
        myCursor = data;
        loadingSpinner.setVisibility(View.GONE);
        listView.setVisibility(View.VISIBLE);
        emptyMsgContainer.setVisibility(View.GONE);
    }

    public void clearCursorReferences() {
        mAdapter.swapCursor(null);
        myCursor = null;
    }

}

Нам нужно получить ссылку на методы интерфейса ListenerFragment, которые реализует MainActivity, чтобы сообщить ему, что FragmentOne собирается запустить загрузчик. Мы получаем эту ссылку через MainFragment. Почему? потому что мы не можем получить его напрямую из FragmentOne.onAttach(Activity activity), так как он вызывается только при первом запуске приложения, а фрагмент не уничтожается и не отсоединяется, при изменении ориентации фрагмент переходит с onDestroyView() на onCreateView(). onAttach() не вызывается.

С другой стороны, MainFragment также не уничтожается (setRetainInstance(true)), но он отсоединяется от старой MainActivity и снова присоединяется к новой MainActivity после завершения изменения ориентации. Мы используем onAttach() для хранения ссылки и создаем метод получения, чтобы фрагменты внутри ViewPager могли получить эту ссылку:

public class MainFragment extends Fragment implements OnClickListener {

    private ListenerFragments listenerFragments;

    @Override
    public void onAttach(Activity myActivity) {
        super.onAttach(myActivity);
        this.listenerFragments = (ListenerFragments)myActivity;
    }

    public ListenerFragments getListenerFragments() {
            return listenerFragments;
    }

}

Зная это, мы можем вернуться к FragmentOne.onResume(), где получим ссылку на MainFragment:

MainFragment parentFragment = (MainFragment)
    getActivity().getSupportFragmentManager().findFragmentByTag("MAIN_FRAGMENT");

Мы используем созданный нами метод получения MainFragment, чтобы получить доступ к методам MainActivity:

    ListenerFragments listenerFragments = (ListenerFragments)parentFragment.getListenerFragments();
    listenerFragments.setFragmentOne(this);
    listenerFragments.prepareLoader(1);

и это в основном все.

person ILovemyPoncho    schedule 27.04.2014