Search code examples
androidandroid-fragmentsandroid-activitykotlinandroid-savedstate

savedInstanceState not restoring Fragment properly


Why is it that my fragment returns a blank screen whenever I use savedInstanceState with it? I've already included the relevant savedInstanceStatecode in my activity, but the associated fragment still doesn't appear at all.

class MyActivity : AppCompatActivity() {
    private var mCurrentValue: Boolean = false

    private var mTwoPane: Boolean = false

    private var activityRecreated: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        mCurrentValue = mSharedPreferences.getBoolean("preference_a", false)
        when {
            mCurrentValue -> setTheme(R.style.MyDarkTheme)
            else -> setTheme(R.style.MyLightTheme)
        }

        super.onCreate(savedInstanceState)

        activityRecreated = savedInstanceState != null

        setContentView(R.layout.md)
    }

    override fun onStart() {
        super.onStart()

        setContentView(R.layout.md)

        mTwoPane = findViewById<View>(R.id.detail_container) != null

        val mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        val mNewValue = mSharedPreferences.getBoolean("preference_a", false)
        when {
            mCurrentValue != mNewValue -> recreate()
        }

        val mToolbar = findViewById<Toolbar>(R.id.my_toolbar)
        setSupportActionBar(mToolbar) 

        if (activityRecreated) {
            val newFragment = MyFragment()
            val transaction = supportFragmentManager.beginTransaction()
            transaction.replace(R.id.master_container, newFragment)
            transaction.commit()
        }
    }
}

Solution

  • There are a few problems here.

    1. You should be setting your content view in onCreate(), not onStart(). onStart() can be invoked multiple times for the same Activity instance. For instance, if you start your Activity, press the Home button, and then resume your app, you'll go through onPause(), onStop(), then onStart(), onResume(). You only need to initialize your view when the Activity is created.

    2. Your logic to display the Fragment only executes if the Activity is being recreated. I think you likely meant the inverse. You could simply change that to be if (!activityRecreated) but I would instead suggest cleaning this up by moving your view initialization entirely into onCreate() like so, and only checking if the theme state has changed in onStart():


    class MyActivity : AppCompatActivity() {
        private val useDarkTheme: Boolean = false
        private var twoPane: Boolean = false
    
        override fun onCreate(savedInstanceState: Bundle?) {
            useDarkTheme = shouldUseDarkTheme()
            setTheme(if (useDarkTheme) R.style.MyDarkTheme else R.style.MyLightTheme)
    
            super.onCreate(savedInstanceState)
            setContentView(R.layout.md)
    
            // savedInstanceState will be null only the first time the Activity is created
            if (savedInstanceState == null) {
                supportFragmentManager.beginTransaction()
                    .replace(R.id.master_container, MyFragment())
                    .commit()
            }
    
            twoPane = findViewById<View>(R.id.detail_container) != null
            setSupportActionBar(findViewById(R.id.my_toolbar))
        }
    
        override fun onStart() {
            super.onStart()
    
            if (useDarkTheme != shouldUseDarkTheme()) {
                recreate()
            }
        }
    
        private fun shouldUseDarkTheme(): Boolean = 
            PreferenceManager.getDefaultSharedPreferences(this).getBoolean("preference_a", false)
    }