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.