IllegalStateException Can not perform this action after onSaveInstanceState show DialogFragment

11.7k views Asked by At

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...)

1

There are 1 answers

7
Bram Stoker On

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 a Fragment 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:

  1. Commit your FragmentTransaction's allowing state loss with commitAllowingStateLoss(). Note that instead of calling show() on your DialogFragment you can do your own FragmentTransaction, because show() is doing exactly that, see the source code.
  2. Check whether the state already has been saved before doing a FragmentTransaction by calling isStateSaved() on your FragmentManager.

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 a Fragment.