Вложенные фрагменты - IllegalStateException "Не удается выполнить это действие после onSaveInstanceState"

Фон

Асинхронные обратные вызовы в Android

Попытка выполнить асинхронную операцию надежным образом на Android излишне запутана, т.е. Является ли AsyncTask действительно концептуально ошибочным или я что-то не хватает?

Теперь это все до введения Фрагментов. С введением фрагментов onRetainNonConfigurationInstance() устарел. Таким образом, последний взломанный Google взломам заключается в использовании постоянного фрагмента, отличного от UI, который прикрепляет/отключает вашу активность при изменении конфигурации (т.е. Вращение экрана, изменение языковых настроек и т.д.).

Пример: https://code.google.com/p/android/issues/detail?id=23096#c4

IllegalStateException - Не удается выполнить это действие после onSaveInstanceState

Теоретически взломать выше позволяет обойти ужасные:

IllegalStateException - "Can not perform this action after onSaveInstanceState"

потому что постоянный не-UI-фрагмент будет получать обратные вызовы для onViewStateRestored() (альтернативно onResume) и onSaveInstanceState() (альтернативно onPause). Как таковой вы можете узнать, когда состояние экземпляра сохраняется/восстанавливается. Это справедливый бит кода для чего-то такого простого, но, используя эти знания, вы можете поставить очередь своих асинхронных обратных вызовов до тех пор, пока у функции FragmentManager не будет установлена ​​переменная mStateSaved до false.

mStateSaved - это переменная, которая в конечном счете несет ответственность за запуск этого исключения.

private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

Итак, теоретически, теперь вы знаете, когда безопасно выполнять транзакции фрагментов, и поэтому вы можете избежать опасного исключения IllegalStateException.

Неправильно!

Вложенные фрагменты

Решение выше работает только для Activity FragmentManager. У самих фрагментов также есть дочерний менеджер фрагментов, который используется для вложения фрагментов. К сожалению, администраторы дочерних фрагментов не синхронизируются с менеджером фрагментов активности вообще. Поэтому, пока менеджер фрагментов активности обновлен и всегда имеет правильный mStateSaved; дочерние фрагменты думают иначе и будут счастливо бросать страшное исключение IllegalStateException в неподходящее время.

Теперь, если вы посмотрели файлы Fragment.java и FragmentManager.java в библиотеке поддержки, вы никоим образом не удивляетесь, что все и что-то связанное с фрагментами подвержено ошибкам. Дизайн и качество кода... ах, сомнительно. Вам нравится booleans?

mHasMenu = false
mHidden = false
mInLayout = false
mIndex = 1
mFromLayout = false
mFragmentId = 0
mLoadersStarted = true
mMenuVisible = true
mNextAnim = 0
mDetached = false
mRemoving = false
mRestored = false
mResumed = true
mRetainInstance = true
mRetaining = false
mDeferStart = false
mContainerId = 0
mState = 5
mStateAfterAnimating = 0
mCheckedForLoaderManager = true
mCalled = true
mTargetIndex = -1
mTargetRequestCode = 0
mUserVisibleHint = true
mBackStackNesting = 0
mAdded = true

В любом случае, немного отвлечься от темы.

Мертвое решение

Итак, вы можете подумать, что решение проблемы просто, которое на данный момент похоже на антоним, добавить еще один из этих хороших хакерских фрагментов без интерфейса для ваших дочерних менеджеров фрагментов. Предположительно, его обратные вызовы синхронизируются со своим внутренним состоянием, и все будет денди.

Неправильно снова!

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

Мой вопрос

Есть ли у кого-нибудь решение для определения, когда безопасно выполнять транзакции фрагментов на дочерних фрагментах в сочетании с асинхронными обратными вызовами кода?

Ответ 1

Обновление 2

React Native

Если вы можете в желудке, используйте React Native. Я знаю, я знаю... "грязные веб-технологии", но со всей серьезностью Android SDK - это катастрофа, поэтому проглотите свою гордость и просто отпустите ее. Вы можете удивить себя; Я знаю, что сделал!

Нельзя или не использовать React Native

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

Вместо этого попробуйте один из:

  • Перейдите к простой системе передачи сообщений, основанной на LocalBroadcastReceiver и имеющей долгоживущие объекты (обычные Java-классы или службы Android), выполняйте ваши запросы и запускайте события, когда изменяется локальное состояние вашего приложения. Затем в вашем Activity/Fragment просто прослушайте определенные Intent и соответствующим образом обновите.
  • Используйте библиотеку активных событий (например, RxJava). Я не пробовал это самостоятельно на Android, но имел довольно хороший успех, используя аналогичную концептуальную библиотеку, ReactiveCocoa для Mac/настольного приложения. По общему признанию, эти библиотеки имеют довольно крутую кривую обучения, но подход становится достаточно освежающим, как только вы привыкнете к нему.

Обновление 1: быстрое и грязное (официальное) решение

Я считаю, что это последнее официальное решение от Google. Однако решение действительно не очень хорошо масштабируется. Если вам не комфортно возиться с очередями, обработчики и сохраненный экземпляр заявляют о себе, это может быть вашим единственным вариантом... но не говорите, что я вас не предупреждал!

Действия и фрагменты Android поддерживают LoaderManager, которые можно использовать с AsyncTaskLoader. За кулисами менеджеров загрузчиков сохраняются точно так же, как и оставшиеся фрагменты. Таким образом, это решение действительно имеет немного общего с моим собственным решением ниже. AsyncTaskLoader - это частично готовое решение, которое технически работает. Однако API чрезвычайно громоздкий; как я уверен, вы заметите в течение нескольких минут после его использования.

Мое решение

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

Я использую сохраненный фрагмент, который добавляется в диспетчер фрагмента активности (или в моем случае - менеджер фрагмента поддержки). Это тот же самый метод, который упоминается в моем вопросе. Этот фрагмент действует как поставщик сорта, который отслеживает, к какой активности он подключен, и имеет очередь сообщений и Runnable (фактически пользовательский подкласс). Очереди выполняются, когда состояние экземпляра больше не сохраняется, и соответствующий обработчик (или исполняемый файл) "готов к выполнению".

Каждый обработчик /runnable хранит UUID, который ссылается на пользователя. Потребители обычно являются фрагментами (которые могут быть надежно закреплены) где-то в пределах активности. Когда пользовательский фрагмент привязан к активности, он ищет фрагмент провайдера и регистрирует себя, используя свой UUID.

Важно, чтобы вы использовали какую-то абстракцию, например UUID, вместо ссылки на потребителей (т.е. фрагменты) напрямую. Это связано с тем, что фрагменты уничтожаются и часто воссоздаются, и вы хотите, чтобы ваши обратные вызовы имели "ссылку" на новые фрагменты; а не старые, принадлежащие к разрушенной деятельности. Как таковые, к сожалению, вы редко можете безопасно использовать переменные, захваченные анонимными классами. Опять же, это связано с тем, что эти переменные могут ссылаться на старый уничтоженный фрагмент или активность. Вместо этого вы должны спросить поставщика для потребителя, который соответствует UUID, который хранил обработчик. Затем вы можете передать этого потребителя любому фрагменту/объекту, который он на самом деле, и использовать его безопасно, так как вы знаете его последний фрагмент с действующим Контекстом (активностью).

Обработчик (или runnable) будет "готов к выполнению", когда потребитель (упомянутый UUID) готов. Необходимо проверить, готов ли потребитель в дополнение к провайдеру, потому что, как упоминалось в моем вопросе, фрагмент потребителя может полагать, что его состояние экземпляра сохраняется, даже если провайдер говорит иначе. Если потребитель (или поставщик) не готов, вы помещаете сообщение (или выполняемое) в очередь в провайдере.

Когда потребительский фрагмент достигает значения onResume(), он информирует провайдера о том, что он готов потреблять поставленные в очередь сообщения /runnables. В этот момент поставщик может попытаться выполнить что-либо в своих очередях, принадлежащих потребителю, который только что был готов.

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

Заключение

Решение довольно запутанное, однако оно работает безупречно, как только вы выработаете, как его реализовать. Если кто-то придумает более простое решение, я был бы рад его услышать.