Вложенные фрагменты - IllegalStateException "Не удается выполнить это действие после onSaveInstanceState"
Фон
Асинхронные обратные вызовы в Android
Попытка выполнить асинхронную операцию надежным способом на Android излишне запутана, т. Е. Действительно ли AsyncTask концептуально ошибочен или я просто что-то упустил?
Теперь это все до введения фрагментов. С введением фрагментов функция onRetainNonConfigurationInstance() устарела. Таким образом, последним хакером от Google является использование постоянного фрагмента, не являющегося пользовательским интерфейсом, который присоединяется / отсоединяется от вашей активности при изменении конфигурации (например, поворот экрана, изменение языковых настроек и т. Д.)
Пример: https://code.google.com/p/android/issues/detail?id=23096
IllegalStateException - не может выполнить это действие после onSaveInstanceState
Теоретически взлом выше позволяет вам обойти страшные:
IllegalStateException - "Can not perform this action after onSaveInstanceState"
потому что постоянный фрагмент не-пользовательского интерфейса будет получать обратные вызовы для 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.
Неправильно!
Вложенные фрагменты
Приведенное выше решение работает только для FragmentManager. Сами фрагменты также имеют дочерний менеджер фрагментов, который используется для вложения фрагментов. К сожалению, дочерние менеджеры фрагментов вообще не синхронизируются с менеджером фрагментов Activity. Так что, хотя менеджер фрагментов деятельности обновлен и всегда имеет правильный mStateSaved; дочерние фрагменты думают иначе и с радостью выбросят страшную исключительную ситуацию IllegalStateException в неподходящее время.
Теперь, если вы посмотрели на Fragment.java и FragmentManager.java в библиотеке поддержки, вы никоим образом не удивитесь, что все, что связано с фрагментами, подвержено ошибкам. Дизайн и качество кода... ах, сомнительно. Тебе нравятся логические выражения?
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) выполняют ваши запросы и запускают события, когда изменяется локальное состояние вашего приложения. Тогда в вашей Деятельности / Фрагмент, просто послушайте навернякаIntent
и обновлять соответственно. - Используйте Реактивную библиотеку событий (например, RxJava). Я сам не пробовал это на Android, но имел довольно хороший успех, используя аналогичную концептуальную библиотеку ReactiveCocoa для Mac/ настольных приложений. По общему признанию у этих библиотек есть довольно крутая кривая изучения, но подход достаточно освежает, когда вы привыкаете к нему.
Обновление 1: быстрое и грязное (официальное) решение
Я считаю, что это последнее официальное решение от Google. Однако решение на самом деле не очень хорошо масштабируется. Если вам неудобно возиться с очередями, обработчиками и сохраненными состояниями экземпляров самостоятельно, тогда это может быть вашим единственным вариантом... но не говорите, что я вас не предупреждал!
Действия и фрагменты Android имеют поддержку LoaderManager, который можно использовать с AsyncTaskLoader. За кулисами менеджеры загрузчика сохраняются точно так же, как и сохраненные фрагменты. Таким образом, это решение имеет немного общего с моим собственным решением ниже. AsyncTaskLoader - это частично готовое решение, которое работает технически. Тем не менее, API является чрезвычайно громоздким; как я уверен, вы заметите в течение нескольких минут его использования.
Мое решение
Во-первых, мое решение отнюдь не просто для реализации. Однако, как только ваша реализация заработает, ее будет легко использовать, и вы сможете настроить ее по своему вкусу.
Я использую сохраненный фрагмент, который добавляется в диспетчер фрагментов действия (или в моем случае поддерживает диспетчер фрагментов). Это та же самая техника, которая упоминается в моем вопросе. Этот фрагмент выступает в качестве своего рода поставщика, который отслеживает, к какой активности он подключен, и имеет очереди Message и Runnable (фактически пользовательский подкласс). Очереди будут выполняться, когда состояние экземпляра больше не будет сохранено, и соответствующий обработчик (или запускаемый) "готов к выполнению".
Каждый обработчик / исполняемый объект хранит UUID, который ссылается на потребителя. Потребители, как правило, представляют собой фрагменты (которые могут быть безопасно вложены) где-то в рамках действия. Когда потребительский фрагмент присоединяется к действию, он ищет фрагмент поставщика и регистрирует себя, используя свой UUID.
Важно, чтобы вы использовали какую-то абстракцию, например, UUID, вместо прямой ссылки на потребителей (то есть фрагменты). Это потому, что фрагменты часто уничтожаются и воссоздаются, и вы хотите, чтобы ваши обратные вызовы имели "ссылку" на новые фрагменты; не старые, которые принадлежат разрушенной деятельности. Поэтому, к сожалению, вы редко можете безопасно использовать переменные, захваченные анонимными классами. Опять же, это потому, что эти переменные могут ссылаться на старый уничтоженный фрагмент или действие. Вместо этого вы должны спросить поставщика о потребителе, который соответствует UUID, сохраненному обработчиком. Затем вы можете привести этого потребителя к любому фрагменту / объекту, которым он на самом деле является, и безопасно использовать его, поскольку вы знаете, что это последний фрагмент с действительным контекстом (действием).
Обработчик (или запускаемый) будет "готов к выполнению", когда потребитель (на который ссылается UUID) готов. Необходимо проверить, готов ли потребитель в дополнение к провайдеру, потому что, как упоминалось в моем вопросе, фрагмент потребителя мог бы полагать, что его состояние экземпляра сохранено, даже если провайдер говорит иначе. Если потребитель (или поставщик) не готов, вы помещаете Сообщение (или работоспособное) в очередь в поставщике.
Когда фрагмент потребителя достигает onResume(), он сообщает провайдеру, что он готов использовать сообщения /runnable s, находящиеся в очереди. В этот момент поставщик может попытаться выполнить в своих очередях все, что принадлежит потребителю, который только что стал готовым.
Это приводит к тому, что обработчики всегда выполняются с использованием действительного контекста (активность, на которую ссылается провайдер) и последнего действительного фрагмента (он же "потребитель").
Заключение
Решение довольно запутанное, однако оно работает без нареканий, как только вы решите, как его реализовать. Если кто-то придумает более простое решение, я буду рад его услышать.