2

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

Itoun
  • 3,713
  • 5
  • 27
  • 47

1 Answers1

4

The purposes 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 because 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 an own FragmentTransaction, because show() is doing exactly that, see the source code.
  2. Check whether 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.

Bram Stoker
  • 1,202
  • 11
  • 14
  • Hello, thanks for your time. actually I already have a ViewModel and my pullChallenge method is already in my ViewModel with my data (listof Challenge) so that's why I'm a bit lost in this situation. So if the state already have been saved how can I use FragnentTransaction ? – Itoun Oct 12 '20 at 14:42
  • Most common way is to use `LiveData` to wrap your `List` and let your `Activity` observe it. However this is not a quick fix. This blog shows an example: https://firebase.googleblog.com/2017/12/using-android-architecture-components.html – Bram Stoker Oct 13 '20 at 07:49
  • Also read up on the Android ViewModel developer documentation, follow the link in my answer. If you get stuck post a new question. – Bram Stoker Oct 13 '20 at 07:54
  • I fixed the 2nd problem with the viewmodel. But there is no data pulling/fetching in my first problem. – Itoun Oct 13 '20 at 20:32
  • Check `isStateSaved()` before showing your dialog, see my answer. Note that splash screens are considered bad UX design. You purposely delay your user for the sake of branding which you already can do on the app icon/colors. – Bram Stoker Oct 14 '20 at 07:54
  • My other way was to use an Handler to delay the display of the popup by the time I want but I have the same problem. Do you u have any other logic to delay the display of any view ? – Itoun Oct 14 '20 at 11:29
  • In My case, In the fragment, getActivity().getSupportFragmentManager().popBackStackImmediate(); was called with a 3s delay on observe of some network response, What i had done is, to now save a boolean livedata in the ViewModel, and observe it in activity, so now, it becomes: shouldPopBackStackImmediate.observe(this@ContextFetchActivity) { if (it != null && it) { supportFragmentManager.popBackStackImmediate() shouldPopBackStackImmediate.value = false } In the activity, will it work? – Vikas Pandey Jan 30 '23 at 05:28