This is the very first time I ever encounter this problem.
I already took a long look to the several answers on SO, especially this one and this one but it didn't solve my problem and most of the answers can't be used as a safe and robust way to solve my cases.
I already tried to:
- override
onSaveInstanceState
and do not call super
but commitAllowingStateLoss can't be used on the first case.
I'm looking for an explanation on how to AVOID to get this exception thrown and how to achieve the action which has thrown the exception (in the first case, show the dialogFragment). I already got how this exception is thrown, however, I don't know what it is thrown in my situation. It appears twice in my app:
The first one occurs in a very simple activity, I have a simple animation and at the end of this animation I show a DialogFragment (SplashActivity):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val potentialLanguage = storage.getString(Constants.LANGUAGE)
val lang = if (potentialLanguage.isNotEmpty()) {
potentialLanguage
} else {
Locale.getDefault().language
}
val language = Language.getFromName(lang)!!
val dm = res.displayMetrics
val conf = res.configuration
conf.setLocale(Locale(language))
saveLanguage(context, lang)
// Use conf.locale = new Locale(...) if targeting lower versions
res.updateConfiguration(conf, dm)
initWarningDialog()
RevelyGradient
.radial()
.colors(
intArrayOf(
getColor(R.color.backgroundOnBoardingStart),
getColor(R.color.backgroundOnBoardingEnd)
)
)
.onBackgroundOf(root)
ivCap.animate()
.alpha(1f)
.setListener(object : Animator.AnimatorListener{
override fun onAnimationEnd(p0: Animator?) {
try {
commonDialog.show(supportFragmentManager, "CommonDialogSplash") //crash here commonDialog is a DialogFragment
}
catch (e: IllegalStateException){
try {
startActivity(Intent(this@SplashActivity, MainActivity::class.java))
finish()
}
catch (e: IllegalStateException){
}
}
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationRepeat(p0: Animator?) {
}
override fun onAnimationStart(p0: Animator?) {
}
}).duration = 1000
}
private fun initWarningDialog(){
commonDialog.isCancelable = false
commonDialog.setTitle(getString(R.string.warning))
commonDialog.setFirstTextButton(getString(R.string.ok))
commonDialog.setDescription(getString(R.string.warning_message))
commonDialog.setFirstButtonListener(object : CommonDialog.CommonDialogClickListener {
override fun onClick() {
commonDialog.dismiss()
startActivity(Intent(this@SplashActivity, MainActivity::class.java))
finish()
}
})
}
The second one is when I try to add a fragment after a firebase firestore request (TotoFragment):
fun pullChallenges(){
val db = Firebase.firestore
val docRef = db.collection("challenges").document(language.name.toLowerCase(Locale.ROOT))
docRef
.get()
.addOnSuccessListener { result ->
result.data?.let {data ->
data.values.map {values->
val alOfHm = values as ArrayList<HashMap<String, String>>
for (item in alOfHm){
val challenge = Challenge()
Challenge.ChallengeCategory.getValueOf(item["category"]!!)?.let {
challenge.challengeCategory = it
}
Game.GameMode.getValueOf(item["mode"]!!)?.let {
challenge.mode = it
}
challenge.challenge = item["content"]!!
challenges.add(challenge)
}
}
}
ChallengesManager.challenges = challenges
listener.onChallengesReady(true)
}
.addOnFailureListener { exception ->
listener.onChallengesReady(false)
Timber.e("Error getting challenges $exception")
}
}
override fun onChallengesReady(success: Boolean) {
renderLoading()
if (success) {
try {
goToChooseMode()
}
catch (e: IllegalStateException){
}
}
else {
Toast.makeText(requireContext(), getString(R.string.error_get_caps), Toast.LENGTH_SHORT).show()
}
}
private fun goToChooseMode(){
val bundle = Bundle()
bundle.putStringArrayList(Constants.PLAYERS, ArrayList(viewModel.players))
activity.supportFragmentManager
.beginTransaction()
.addToBackStack(ChooseModeFragment::class.java.name)
.setReorderingAllowed(true)
.add(R.id.fragmentContainer, ChooseModeFragment::class.java, bundle, ChooseModeFragment::class.java.name)
.commit()
}
Any help of understand this problem (for the thought, or some explanations on the problem, or quick fix...)
The purpose of saving state is that a user can navigate away from an app and come back later to find the app in exactly the same state as he left it, so he can continue like nothing has happened. In the background, Android can kill your app to free up resources, but the user does not have to know that.
Android already does a lot of state saving for you, like the fragments you add. The reason the
IllegalStateException
is thrown is that you add aFragment
after its state already has been saved, so its state cannot be fully restored again. In both your cases you start a background task and when you are 'called back' the user already navigated away (or did a configuration change, like rotating the device).To handle situations like these you can:
FragmentTransaction
's allowing state loss withcommitAllowingStateLoss()
. Note that instead of callingshow()
on yourDialogFragment
you can do your ownFragmentTransaction
, becauseshow()
is doing exactly that, see the source code.FragmentTransaction
by callingisStateSaved()
on yourFragmentManager
.Solution #2 (no state loss) is better than #1, but it does require you to fetch data from your FireStore twice on configuration changes. A more modern approach would use a ViewModel to hold your data, so you would only need to fetch data once (on configuration changes). This is because a
ViewModel
has a longer (and very convenient) lifecycle than aFragment
.