Kotlin ViewModel and LiveData
ViewModel and LiveData are Android Architecture Components that separate your app's logic from its UI. ViewModel holds and manages UI-related data. LiveData wraps that data and automatically notifies the UI when it changes. Together, they prevent common bugs caused by screen rotations and lifecycle mismatches.
The Problem They Solve
Without ViewModel: User types a search query → rotates screen → Activity recreated from scratch → Search result LOST With ViewModel: User types a search query → rotates screen → Activity recreated → ViewModel survives rotation → Search result STILL THERE
Diagram — ViewModel Survives Rotation
Screen Portrait Rotation Screen Landscape ────────────── ─────── ────────────────── Activity (created) ──┐ ┌── Activity (recreated) ↕ (observes) │ │ ↕ (observes) LiveData │ │ LiveData ↕ │ │ ↕ ViewModel ───────────────┴───── SURVIVES ────┴── ViewModel (same!) data = [search results] data = [still here]
Adding the Dependencies
// build.gradle.kts (app level)
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
implementation("androidx.activity:activity-ktx:1.8.2")
}
Creating a ViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
// MutableLiveData — can change value internally
private val _count = MutableLiveData(0)
// Expose as LiveData — read-only from outside
val count: LiveData = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
fun decrement() {
_count.value = (_count.value ?: 0) - 1
}
fun reset() {
_count.value = 0
}
}
MutableLiveData is private because only the ViewModel should change the data. The Activity only observes through the public LiveData.
Observing LiveData in an Activity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// Get the ViewModel — survives rotation
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Observe: runs whenever count changes
viewModel.count.observe(this) { count ->
binding.tvCount.text = count.toString()
}
binding.btnPlus.setOnClickListener { viewModel.increment() }
binding.btnMinus.setOnClickListener { viewModel.decrement() }
binding.btnReset.setOnClickListener { viewModel.reset() }
}
}
How It Works — Step by Step
Step 1: User taps "+" button
Step 2: Activity calls viewModel.increment()
Step 3: ViewModel updates _count.value = old + 1
Step 4: LiveData detects the change
Step 5: observe { } block runs automatically
Step 6: tvCount.text updates on screen
Activity never holds the count value itself.
ViewModel owns the data. Activity displays it.
ViewModel with a Repository (Real-World Pattern)
In real apps, the ViewModel does not fetch data directly. It asks a Repository, which decides whether to get data from the network or local database.
// Repository — single source of truth
class UserRepository {
suspend fun getUser(id: Int): User {
// In a real app: call an API here
return User(id, "Priya Sharma", "priya@estudy247.com")
}
}
data class User(val id: Int, val name: String, val email: String)
// ViewModel uses the repository
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
fun loadUser(id: Int) {
viewModelScope.launch { // coroutine tied to ViewModel
val result = repo.getUser(id)
_user.postValue(result) // update from background thread
}
}
}
Diagram — MVVM Architecture
┌───────────────────────────────────────────────────┐
│ Activity / Fragment │
│ (UI — displays data, sends user events to VM) │
└──────────────────────┬────────────────────────────┘
│ observes LiveData
▼
┌───────────────────────────────────────────────────┐
│ ViewModel │
│ (holds UI state, calls repository, no View refs) │
└──────────────────────┬────────────────────────────┘
│ requests data
▼
┌───────────────────────────────────────────────────┐
│ Repository │
│ (decides: network or database?) │
└────────┬──────────────────────┬───────────────────┘
│ │
▼ ▼
Remote API Local Database
(Retrofit) (Room)
LiveData vs StateFlow
Feature | LiveData | StateFlow --------------------|----------------------|------------------------- Lifecycle-aware | Yes | Manual (with repeatOnLifecycle) Null value | Allowed | Not allowed (needs initial) Default value | None required | Required in constructor Used with | observe() | collect() in coroutine Modern preference | Older projects | New projects (recommended)
