Saving and Restoring Android UI State: Advanced Kotlin XML Techniques

In Android development, particularly with Kotlin and XML, managing UI state across configuration changes and app restarts is crucial for a smooth user experience. While ViewModel effectively handles UI-related data, more intricate scenarios require additional mechanisms to preserve UI state. This blog post explores advanced techniques for saving and restoring UI state beyond ViewModel, focusing on XML-based layouts.

Why UI State Management is Essential

UI state refers to the information representing the current status of your app’s user interface, such as entered text, scroll positions, and selection states. Properly managing UI state ensures that your app retains this information when:

  • The device orientation changes.
  • The app is killed and restarted by the system.
  • Users switch between apps.

Challenges Beyond ViewModel

While ViewModel handles configuration changes well, it does not persist data across app restarts. For persisting UI state beyond a single session, you need to employ other methods.

Techniques for Saving and Restoring UI State

1. onSaveInstanceState() and onRestoreInstanceState()

The most basic mechanism for saving UI state in Android is using the onSaveInstanceState() and onRestoreInstanceState() methods in Activity or Fragment.

Example: Saving and Restoring EditText State

import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var editText: EditText
    private val editTextKey = "editTextContent"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        editText = findViewById(R.id.editText)

        // Restore state if available
        savedInstanceState?.let {
            val savedText = it.getString(editTextKey, "")
            editText.setText(savedText)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString(editTextKey, editText.text.toString())
    }
}

XML Layout (activity_main.xml):


<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Enter Text" />

In this example, the EditText‘s content is saved in onSaveInstanceState() using a unique key and restored in onCreate() if savedInstanceState is not null. This method is ideal for small pieces of UI data.

2. Using SavedStateHandle with ViewModel

SavedStateHandle is a class in the ViewModel component that allows you to save and restore UI state across configuration changes *and* process death.

Step 1: Add Dependency

Add the necessary dependency to your build.gradle file:


dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1"
}
Step 2: Implement ViewModel with SavedStateHandle

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel

class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    private val editTextKey = "editTextContent"

    var editTextContent: String = savedStateHandle[editTextKey] ?: ""
        set(value) {
            field = value
            savedStateHandle[editTextKey] = value
        }
}
Step 3: Use the ViewModel in your Activity

import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {

    private lateinit var editText: EditText
    private val viewModel: MyViewModel by viewModels {
        object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return MyViewModel(SavedStateHandle()) as T
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        editText = findViewById(R.id.editText)
        editText.setText(viewModel.editTextContent)

        editText.doAfterTextChanged { text ->
            viewModel.editTextContent = text.toString()
        }
    }
}

XML Layout (activity_main.xml) remains the same.

SavedStateHandle survives configuration changes and process death, providing a robust way to manage UI state within a ViewModel.

3. Using rememberSaveable in Jetpack Compose (Applicable if integrating Compose)

While the focus is on XML layouts, it’s valuable to know about Compose’s rememberSaveable for future hybrid approaches. It automatically saves the state across configuration changes and process death using a Saver object.


import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberSaveable
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable

@Composable
fun MyComposable() {
    val (text, setText) = rememberSaveable { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = setText
    )
}

In this example, rememberSaveable preserves the state of the TextField even across configuration changes and process death.

4. Saving to Local Storage (SharedPreferences or Database)

For more persistent storage that survives app uninstalls, use SharedPreferences or a local database. This approach is suitable for complex states or user preferences that need to persist long-term.

Saving to SharedPreferences

import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var editText: EditText
    private lateinit var sharedPreferences: SharedPreferences
    private val editTextKey = "editTextContent"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        editText = findViewById(R.id.editText)
        sharedPreferences = getPreferences(Context.MODE_PRIVATE)

        val savedText = sharedPreferences.getString(editTextKey, "")
        editText.setText(savedText)
    }

    override fun onPause() {
        super.onPause()
        with (sharedPreferences.edit()) {
            putString(editTextKey, editText.text.toString())
            apply()
        }
    }
}

XML Layout (activity_main.xml) remains the same.

Here, SharedPreferences is used to store the text in onPause() and retrieve it in onCreate(), persisting it even when the app is closed and reopened.

Best Practices

  • Use onSaveInstanceState() for Simple UI Elements: Suitable for preserving basic UI states like text in an EditText.
  • Employ SavedStateHandle with ViewModel: Ideal for more robust UI state management that survives both configuration changes and process death.
  • Consider SharedPreferences or Databases: For long-term persistence that outlives app sessions.
  • Handle Large Data Sets Carefully: Avoid storing large bitmaps or extensive datasets in onSaveInstanceState() or SavedStateHandle; opt for databases or external storage.
  • Test Thoroughly: Always test state restoration by simulating configuration changes and app restarts to ensure data integrity.

Conclusion

Efficiently managing UI state in Kotlin and XML Android development requires understanding the strengths and limitations of various techniques. From simple methods like onSaveInstanceState() to more sophisticated approaches using SavedStateHandle and local storage, each option serves a unique purpose. By selecting the right method for your specific use case, you can provide a seamless and persistent user experience, retaining crucial UI data across different app states and system events. Implementing a well-thought-out state management strategy enhances the reliability and user-friendliness of your Android applications.