Skip to main content

Kotlin ViewModels

Introduction

When developing Android applications with Kotlin, managing UI-related data effectively is crucial for building robust and responsive apps. ViewModels are a fundamental component of Android's Architecture Components that help separate the UI data management from UI controllers (Activities and Fragments).

In this tutorial, we will learn what ViewModels are, why they're important, and how to implement them in your Kotlin Android applications. By the end, you'll understand how ViewModels help solve common Android development challenges like handling configuration changes (screen rotations) and maintaining a clean architecture.

What are ViewModels?

A ViewModel is a class that is responsible for preparing and managing data for an Activity or Fragment. ViewModels are designed to store and manage UI-related data in a lifecycle-conscious way. They allow data to survive configuration changes such as screen rotations.

Key Benefits of ViewModels

  • Survive configuration changes: Data is retained when the Activity or Fragment is recreated
  • Separation of concerns: Clear separation between UI controllers and UI data management
  • Lifecycle awareness: Automatically cleared when the associated lifecycle owner is destroyed
  • Data sharing: Can be shared between Fragments in an Activity

Setting Up ViewModels

Step 1: Add dependencies

First, you need to add the ViewModel dependency to your project. Open your app-level build.gradle file and add:

gradle
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
implementation "androidx.activity:activity-ktx:1.7.1"
implementation "androidx.fragment:fragment-ktx:1.6.0"
}

Don't forget to sync your project after adding these dependencies.

Step 2: Create a ViewModel class

Let's create a simple ViewModel for a counter application:

kotlin
import androidx.lifecycle.ViewModel

class CounterViewModel : ViewModel() {
// Data that needs to survive configuration changes
private var _counter = 0

// Public getter for the counter
val counter: Int
get() = _counter

// Functions to modify the data
fun increment() {
_counter++
}

fun decrement() {
if (_counter > 0) {
_counter--
}
}

fun reset() {
_counter = 0
}
}

Step 3: Using ViewModels in Activities

To use the ViewModel in an Activity, you need to get a reference to it using ViewModelProvider:

kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels

class MainActivity : AppCompatActivity() {
// Get a reference to the ViewModel using the viewModels() delegate
private val viewModel: CounterViewModel by viewModels()

private lateinit var counterTextView: TextView
private lateinit var incrementButton: Button
private lateinit var decrementButton: Button
private lateinit var resetButton: Button

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

// Initialize UI components
counterTextView = findViewById(R.id.counterTextView)
incrementButton = findViewById(R.id.incrementButton)
decrementButton = findViewById(R.id.decrementButton)
resetButton = findViewById(R.id.resetButton)

// Update UI with the current counter value
updateCounterDisplay()

// Set up button click listeners
incrementButton.setOnClickListener {
viewModel.increment()
updateCounterDisplay()
}

decrementButton.setOnClickListener {
viewModel.decrement()
updateCounterDisplay()
}

resetButton.setOnClickListener {
viewModel.reset()
updateCounterDisplay()
}
}

private fun updateCounterDisplay() {
counterTextView.text = "Count: ${viewModel.counter}"
}
}

Step 4: Using ViewModels in Fragments

Using ViewModels with Fragments is similar to using them with Activities:

kotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels

class CounterFragment : Fragment() {
// Get a reference to the ViewModel using the viewModels() delegate
private val viewModel: CounterViewModel by viewModels()

private lateinit var counterTextView: TextView
private lateinit var incrementButton: Button
private lateinit var decrementButton: Button
private lateinit var resetButton: Button

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_counter, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Initialize UI components
counterTextView = view.findViewById(R.id.counterTextView)
incrementButton = view.findViewById(R.id.incrementButton)
decrementButton = view.findViewById(R.id.decrementButton)
resetButton = view.findViewById(R.id.resetButton)

// Update UI with the current counter value
updateCounterDisplay()

// Set up button click listeners
incrementButton.setOnClickListener {
viewModel.increment()
updateCounterDisplay()
}

decrementButton.setOnClickListener {
viewModel.decrement()
updateCounterDisplay()
}

resetButton.setOnClickListener {
viewModel.reset()
updateCounterDisplay()
}
}

private fun updateCounterDisplay() {
counterTextView.text = "Count: ${viewModel.counter}"
}
}

ViewModels with LiveData

To make our ViewModel more reactive and automatically update the UI when data changes, we can use LiveData with ViewModels.

Step 1: Update the ViewModel to use LiveData

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

class CounterViewModel : ViewModel() {
// MutableLiveData that can be changed within the ViewModel
private val _counter = MutableLiveData<Int>(0)

// Expose as immutable LiveData to observers
val counter: LiveData<Int> = _counter

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

fun decrement() {
val currentValue = _counter.value ?: 0
if (currentValue > 0) {
_counter.value = currentValue - 1
}
}

fun reset() {
_counter.value = 0
}
}

Step 2: Observe LiveData in the Activity/Fragment

In your Activity:

kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels

class MainActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()

private lateinit var counterTextView: TextView
private lateinit var incrementButton: Button
private lateinit var decrementButton: Button
private lateinit var resetButton: Button

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

// Initialize UI components
counterTextView = findViewById(R.id.counterTextView)
incrementButton = findViewById(R.id.incrementButton)
decrementButton = findViewById(R.id.decrementButton)
resetButton = findViewById(R.id.resetButton)

// Observe the LiveData and update UI when it changes
viewModel.counter.observe(this) { count ->
counterTextView.text = "Count: $count"
}

// Set up button click listeners
incrementButton.setOnClickListener { viewModel.increment() }
decrementButton.setOnClickListener { viewModel.decrement() }
resetButton.setOnClickListener { viewModel.reset() }
}
}

Sharing ViewModels Between Fragments

One of the powerful features of ViewModels is the ability to share data between fragments. This is useful for communication between fragments without having to go through the activity.

Step 1: Create a shared ViewModel

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

class SharedViewModel : ViewModel() {
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message

fun setMessage(message: String) {
_message.value = message
}
}

Step 2: Share the ViewModel between fragments

In your first fragment:

kotlin
class FirstFragment : Fragment() {
// Get reference to the ViewModel that's scoped to the activity
private val sharedViewModel: SharedViewModel by activityViewModels()

private lateinit var messageEditText: EditText
private lateinit var sendButton: Button

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

messageEditText = view.findViewById(R.id.messageEditText)
sendButton = view.findViewById(R.id.sendButton)

sendButton.setOnClickListener {
val message = messageEditText.text.toString()
if (message.isNotEmpty()) {
// Update the shared ViewModel
sharedViewModel.setMessage(message)
messageEditText.text.clear()
}
}
}
}

In your second fragment:

kotlin
class SecondFragment : Fragment() {
// Get reference to the same shared ViewModel
private val sharedViewModel: SharedViewModel by activityViewModels()

private lateinit var receivedMessageTextView: TextView

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

receivedMessageTextView = view.findViewById(R.id.receivedMessageTextView)

// Observe the shared data
sharedViewModel.message.observe(viewLifecycleOwner) { message ->
receivedMessageTextView.text = message
}
}
}

ViewModels with SavedStateHandle

Sometimes you need to save data not only across configuration changes but also when the process is killed. SavedStateHandle helps to accomplish this.

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

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

// Define keys for the state
companion object {
private const val COUNTER_KEY = "counter"
}

// Initialize with a default value or the restored state
private var _counter: Int
get() = savedStateHandle.get<Int>(COUNTER_KEY) ?: 0
set(value) {
savedStateHandle[COUNTER_KEY] = value
}

val counter: Int
get() = _counter

fun increment() {
_counter++
}

fun decrement() {
if (_counter > 0) {
_counter--
}
}

fun reset() {
_counter = 0
}
}

To use this ViewModel:

kotlin
// In your Activity or Fragment
private val viewModel: StatefulViewModel by viewModels()

Real-World Example: A Weather App

Let's look at a more practical example of using ViewModels in a weather app:

WeatherViewModel

kotlin
import androidx.lifecycle.*
import kotlinx.coroutines.launch

class WeatherViewModel : ViewModel() {
private val _weatherData = MutableLiveData<WeatherData>()
val weatherData: LiveData<WeatherData> = _weatherData

private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: LiveData<Boolean> = _isLoading

private val _error = MutableLiveData<String?>(null)
val error: LiveData<String?> = _error

private val weatherRepository = WeatherRepository()

fun fetchWeatherForCity(city: String) {
_isLoading.value = true
_error.value = null

viewModelScope.launch {
try {
val result = weatherRepository.getWeatherForCity(city)
_weatherData.value = result
_isLoading.value = false
} catch (e: Exception) {
_error.value = "Failed to fetch weather: ${e.message}"
_isLoading.value = false
}
}
}
}

// Data classes for our weather information
data class WeatherData(
val city: String,
val temperature: Double,
val description: String,
val humidity: Int,
val windSpeed: Double
)

WeatherActivity

kotlin
class WeatherActivity : AppCompatActivity() {
private val viewModel: WeatherViewModel by viewModels()

private lateinit var cityEditText: EditText
private lateinit var searchButton: Button
private lateinit var loadingIndicator: ProgressBar
private lateinit var weatherInfoLayout: LinearLayout
private lateinit var errorTextView: TextView
private lateinit var temperatureTextView: TextView
private lateinit var descriptionTextView: TextView
private lateinit var humidityTextView: TextView
private lateinit var windSpeedTextView: TextView

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

// Initialize UI components
cityEditText = findViewById(R.id.cityEditText)
searchButton = findViewById(R.id.searchButton)
loadingIndicator = findViewById(R.id.loadingIndicator)
weatherInfoLayout = findViewById(R.id.weatherInfoLayout)
errorTextView = findViewById(R.id.errorTextView)
temperatureTextView = findViewById(R.id.temperatureTextView)
descriptionTextView = findViewById(R.id.descriptionTextView)
humidityTextView = findViewById(R.id.humidityTextView)
windSpeedTextView = findViewById(R.id.windSpeedTextView)

// Set up button click listener
searchButton.setOnClickListener {
val city = cityEditText.text.toString().trim()
if (city.isNotEmpty()) {
viewModel.fetchWeatherForCity(city)
}
}

// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
loadingIndicator.visibility = if (isLoading) View.VISIBLE else View.GONE
}

// Observe error state
viewModel.error.observe(this) { errorMessage ->
errorTextView.visibility = if (errorMessage != null) View.VISIBLE else View.GONE
weatherInfoLayout.visibility = if (errorMessage == null) View.VISIBLE else View.GONE
errorTextView.text = errorMessage
}

// Observe weather data
viewModel.weatherData.observe(this) { weatherData ->
// Update UI with weather information
temperatureTextView.text = "Temperature: ${weatherData.temperature}°C"
descriptionTextView.text = "Weather: ${weatherData.description}"
humidityTextView.text = "Humidity: ${weatherData.humidity}%"
windSpeedTextView.text = "Wind Speed: ${weatherData.windSpeed} m/s"
}
}
}

Best Practices for ViewModels

  1. Keep ViewModels UI-agnostic: ViewModels should not contain references to UI components or contexts to avoid memory leaks.

  2. Use LiveData or StateFlow: Make your UI reactive by using observable data holders like LiveData or StateFlow.

  3. Keep ViewModel logic focused: ViewModels should handle UI-related data operations, but delegate business logic to other layers (repositories, use cases).

  4. Handle configuration changes properly: Don't reload data unnecessarily when the device orientation changes.

  5. Use Factories when needed: When your ViewModel requires dependencies, use a ViewModel Factory.

    kotlin
    class MyViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
    @Suppress("UNCHECKED_CAST")
    return MyViewModel(repository) as T
    }
    throw IllegalArgumentException("Unknown ViewModel class")
    }
    }

    Then use it like this:

    kotlin
    val factory = MyViewModelFactory(repository)
    val viewModel: MyViewModel by viewModels { factory }

Summary

ViewModels are an essential component of modern Android development that help you manage UI-related data in a lifecycle-aware way. They provide a clean separation between your UI controllers and the data they display, making your code more maintainable and robust.

In this tutorial, we learned:

  • What ViewModels are and why they're important
  • How to create and use ViewModels in Activities and Fragments
  • How to use LiveData with ViewModels for reactive UIs
  • How to share ViewModels between fragments
  • How to persist data through process death using SavedStateHandle
  • A real-world implementation of ViewModels in a weather app
  • Best practices for working with ViewModels

By following these guidelines, you can create Android applications that handle configuration changes gracefully and maintain a clean architecture.

Additional Resources

Exercises

  1. Create a simple note-taking app that uses a ViewModel to manage the list of notes and survives configuration changes.
  2. Extend the counter example to include a ViewModel Factory that takes an initial count as a parameter.
  3. Create an app with multiple fragments that share information through a shared ViewModel.
  4. Modify the weather app example to cache the results using SavedStateHandle so that data persists even when the process is killed.
  5. Add a search history feature to the weather app that persists across app launches using a ViewModel and Room database.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)