Handling Configuration Changes in Android with ViewModels & XML (Kotlin)

When developing Android applications using Kotlin and XML layouts, handling configuration changes gracefully is crucial for a smooth user experience. Configuration changes, such as screen rotations or keyboard availability changes, can cause the Activity to be recreated, leading to data loss and disrupted workflows. ViewModels are a powerful tool to mitigate these issues, but they require proper implementation and understanding.

Understanding Configuration Changes

Configuration changes are events that occur when the device’s configuration alters, such as:

  • Screen Rotation
  • Keyboard Availability
  • Language Changes
  • Density Changes

By default, Android Activities are recreated during configuration changes. This means the Activity’s state is lost unless specifically handled.

The Role of ViewModels

ViewModels are designed to store and manage UI-related data in a lifecycle-conscious way. The ViewModel class allows data to survive configuration changes. They provide a way to:

  • Retain data across Activity recreations.
  • Provide data to the UI.
  • Handle UI logic separately from the Activity.

Implementation Steps

To handle configuration changes using ViewModels with XML layouts, follow these steps:

Step 1: Add Dependencies

Ensure you have the necessary dependencies in your build.gradle file:

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "androidx.core:core-ktx:1.12.0"
}

Step 2: Create a ViewModel Class

Define a ViewModel class that extends ViewModel. Store the data you want to retain across configuration changes within this class:

import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {
    var counter: Int = 0

    fun incrementCounter() {
        counter++
    }
}

Step 3: Use ViewModel in the Activity

In your Activity, instantiate and use the ViewModel. The ViewModelProvider ensures that the ViewModel survives configuration changes:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import android.widget.TextView
import android.widget.Button
import com.example.myandroidapp.R

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MyViewModel
    private lateinit var counterTextView: TextView
    private lateinit var incrementButton: Button

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

        viewModel = ViewModelProvider(this)[MyViewModel::class.java]

        counterTextView = findViewById(R.id.counterTextView)
        incrementButton = findViewById(R.id.incrementButton)

        updateCounter()

        incrementButton.setOnClickListener {
            viewModel.incrementCounter()
            updateCounter()
        }
    }

    private fun updateCounter() {
        counterTextView.text = "Counter: ${viewModel.counter}"
    }
}

The XML layout file activity_main.xml:




    

    

Step 4: Using LiveData for Dynamic Updates

To make the UI dynamically update when data changes, use LiveData within your ViewModel and observe it in the Activity.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MyLiveDataViewModel : ViewModel() {
    private val _counter = MutableLiveData(0)
    val counter: LiveData = _counter

    fun incrementCounter() {
        _counter.value = (_counter.value ?: 0) + 1
    }
}

Observe the LiveData in the Activity:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import android.widget.TextView
import android.widget.Button

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MyLiveDataViewModel
    private lateinit var counterTextView: TextView
    private lateinit var incrementButton: Button

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

        viewModel = ViewModelProvider(this)[MyLiveDataViewModel::class.java]

        counterTextView = findViewById(R.id.counterTextView)
        incrementButton = findViewById(R.id.incrementButton)

        viewModel.counter.observe(this, Observer { count ->
            counterTextView.text = "Counter: $count"
        })

        incrementButton.setOnClickListener {
            viewModel.incrementCounter()
        }
    }
}

Advanced Techniques

1. Saving and Restoring State

Although ViewModels survive configuration changes, they do not survive process death (e.g., when the system reclaims resources). For such cases, use SavedStateHandle in your ViewModel to save and restore data.

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

class MySavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    private val key = "counter"

    var counter: Int
        get() = savedStateHandle.get(key) ?: 0
        set(value) {
            savedStateHandle.set(key, value)
        }

    fun incrementCounter() {
        counter++
    }
}

2. Using ViewBinding

ViewBinding simplifies XML view access. Enable it in your build.gradle:

android {
    buildFeatures {
        viewBinding = true
    }
}

In the Activity:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import com.example.myandroidapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        viewModel = ViewModelProvider(this)[MyViewModel::class.java]

        binding.counterTextView.text = "Counter: ${viewModel.counter}"

        binding.incrementButton.setOnClickListener {
            viewModel.incrementCounter()
            binding.counterTextView.text = "Counter: ${viewModel.counter}"
        }
    }
}

Best Practices

  • Keep ViewModels lightweight and focused on data management.
  • Avoid holding references to Activities or Fragments in ViewModels to prevent memory leaks.
  • Use LiveData for dynamic UI updates to ensure the UI is always in sync with the data.
  • Consider using SavedStateHandle for retaining data across process deaths.

Conclusion

Handling configuration changes effectively using ViewModels and XML layouts in Kotlin Android development is essential for maintaining a seamless user experience. By storing UI-related data in ViewModels, you can ensure that it survives configuration changes, reducing data loss and improving app reliability. Integrating LiveData and ViewBinding further enhances the development process by providing dynamic UI updates and simplified view access.