In Step 6 you built a RecyclerView that displays a list of data. Now try this: rotate your phone while the list is showing. If you're storing the data directly in the Activity, it just vanished — the Activity was destroyed and recreated, and everything in it went with it.
ViewModel and LiveData are the solution. ViewModel survives screen rotation. LiveData delivers data updates to the UI only when the UI is ready to receive them. Together they form the core of Android's recommended MVVM architecture and the foundation of every serious Android app.
ViewModel — your data's safe house across the full Android lifecycle
Why Your Data Disappears on Rotation
Android destroys and recreates your Activity on every configuration change. It's not a bug — it's by design. The OS needs to re-apply resources for the new configuration (screen size, orientation, language). The full lifecycle fires: onDestroy() → onCreate(). Any data stored directly in the Activity is gone.
Configuration changes that trigger this:
- Screen rotation — portrait to landscape and back
- Keyboard visibility — soft keyboard appearing or disappearing
- Language/locale change — switching system language at runtime
- Dark/light mode toggle — theme change at runtime
ViewModel solves this by living outside the Activity's lifecycle scope. It's created when the Activity first starts and only destroyed when the Activity is permanently finished — not during rotation. The Activity gets recreated and simply re-attaches to the existing ViewModel instance.
1. Add Dependencies
dependencies {
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.0"
// by viewModels() delegate for Activity
implementation "androidx.activity:activity-ktx:1.9.0"
// by viewModels() / activityViewModels() delegate for Fragment
implementation "androidx.fragment:fragment-ktx:1.8.0"
}
2. Create Your First ViewModel
Two rules that matter here. First: keep MutableLiveData private — only the ViewModel can change its value. Second: expose read-only LiveData to the UI. This prevents the Activity or Fragment from directly writing state, which is the most common source of hard-to-trace bugs in Android apps.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
// Private — only this ViewModel writes to it
private val _count = MutableLiveData(0)
// Public read-only — Activity/Fragment observes this
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
fun decrement() {
_count.value = (_count.value ?: 0) - 1
}
fun reset() {
_count.value = 0
}
}
3. Attach ViewModel to Activity
Use by viewModels() — the Kotlin property delegate that handles ViewModel creation and caching automatically. The ViewModel instance is tied to this Activity's lifecycle and survives rotation.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// ViewModel scoped to this Activity — survives rotation automatically
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Observe LiveData — UI updates automatically when value changes
// Only updates when Activity is in STARTED or RESUMED state
viewModel.count.observe(this) { count ->
binding.tvCount.text = count.toString()
}
binding.btnIncrement.setOnClickListener { viewModel.increment() }
binding.btnDecrement.setOnClickListener { viewModel.decrement() }
binding.btnReset.setOnClickListener { viewModel.reset() }
}
}
Rotate the screen. The count stays. That's ViewModel in action.
4. Using ViewModel in Fragments
In Fragments you have two choices depending on whether data needs to be shared:
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.fragment.app.activityViewModels
class MyFragment : Fragment() {
// Fragment-scoped ViewModel — destroyed when this Fragment is destroyed
private val fragmentViewModel: MyViewModel by viewModels()
// Activity-scoped ViewModel — shared with host Activity and all other Fragments
// Use this when two Fragments need to exchange data
private val sharedViewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// IMPORTANT: Always use viewLifecycleOwner in Fragments — not 'this'
// 'this' (the Fragment) outlives its View — causes stacked observers and memory leaks
sharedViewModel.selectedItem.observe(viewLifecycleOwner) { item ->
binding.tvSelected.text = item.name
}
}
}
this instead of viewLifecycleOwner when observing LiveData in a Fragment. The Fragment instance survives beyond its View (e.g. when on the back stack). If you observe with this, the observer accumulates each time the Fragment is shown — causing the callback to fire multiple times per update.
5. Modelling Real UI State with LiveData
Real apps need more than a single value. Model loading, content, and error states as separate LiveData properties — each observed independently by the UI:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User?>(null)
val user: LiveData<User?> = _user
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>(null)
val error: LiveData<String?> = _error
fun loadUser(userId: String) {
_isLoading.value = true
_error.value = null
// In real apps: use viewModelScope + coroutines here (see next section)
// For now, simulating a result:
_user.value = User(id = 1, name = "Pragnesh Ghoda", email = "pragnesh@example.com")
_isLoading.value = false
}
}
// In Activity/Fragment:
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.isVisible = isLoading
}
viewModel.user.observe(this) { user ->
user?.let { binding.tvName.text = it.name }
}
viewModel.error.observe(this) { error ->
error?.let { binding.tvError.text = it }
}
6. ViewModel with Coroutines — The Production Pattern
In real apps, data comes from a network or database asynchronously. Pair ViewModel with Kotlin Coroutines using viewModelScope — coroutines launched here are automatically cancelled when the ViewModel is cleared:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
// StateFlow — modern alternative to LiveData for Kotlin-first code
private val _products = MutableStateFlow<List<Product>>(emptyList())
val products: StateFlow<List<Product>> = _products
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error
init {
loadProducts()
}
fun loadProducts() {
// viewModelScope cancels automatically when ViewModel is cleared
viewModelScope.launch {
_isLoading.value = true
_error.value = null
try {
_products.value = repository.getProducts()
} catch (e: Exception) {
_error.value = e.message ?: "Something went wrong"
} finally {
_isLoading.value = false
}
}
}
}
7. LiveData vs StateFlow — Which Should You Use?
| LiveData | StateFlow | |
|---|---|---|
| Lifecycle-aware by default | ✅ Yes — automatic | ⚠️ Needs repeatOnLifecycle |
| Initial value required | ❌ Optional | ✅ Always required |
| Kotlin-first | ⚠️ Java origins | ✅ Pure Kotlin Coroutines |
| Jetpack Compose support | ⚠️ Via observeAsState() |
✅ Native with collectAsStateWithLifecycle() |
| Best for | Beginners, simple View-based UI | Kotlin-first projects, Coroutines, Compose |
For this series: use LiveData while learning — it handles lifecycle automatically with no extra boilerplate. Migrate to StateFlow when you're comfortable with Coroutines.
Best Practices
- Never hold a Context or View reference in ViewModel — the ViewModel outlives the Activity. Holding a reference to the Activity or any View causes a memory leak. If you genuinely need Application context, extend
AndroidViewModelinstead. - Expose immutable data to the UI —
MutableLiveData/MutableStateFlowstays private inside the ViewModel. The UI only observes the read-only version. This makes data flow predictable and prevents bugs caused by the UI directly mutating state. - Use
viewModelScopefor all coroutines — coroutines launched here are automatically cancelled when the ViewModel is cleared. No manual cleanup needed. See our Kotlin Coroutines guide for the full pattern. - Always use
viewLifecycleOwnerin Fragments — without exception. Usingthisleads to stacked observers and memory leaks every time the Fragment is re-shown. - One ViewModel per screen — don't share a ViewModel across unrelated screens. Use
by activityViewModels()only for data genuinely shared between Fragments on the same screen.
Try It Yourself
- Test rotation survival — build a counter app that increments a number stored in a ViewModel. Rotate the screen and verify the count stays. Then move the count directly into the Activity and rotate again — observe it resets to 0.
- Connect ViewModel to RecyclerView — take your Step 6 RecyclerView, move the user list into a ViewModel as a
MutableLiveData<List<User>>, observe it in the Activity, and calladapter.submitList()in the observer. Add a "Shuffle" button that shuffles the list in the ViewModel and watch DiffUtil animate the result. - Share data between Fragments — build two Fragments. In Fragment A add a text input and a "Send" button. On click, update a shared ViewModel. In Fragment B observe the same ViewModel and display the text. Use
by activityViewModels()in both Fragments.
Your UI data survives rotation - but not process death. In Step 8: Room Database - Permanent Local Storage we learn how to store data permanently on the device so it's still there even after the app is killed.
Frequently Asked Questions
Does ViewModel survive process death?
No — ViewModel only survives configuration changes like rotation. If the system kills the process due to low memory, ViewModel data is lost. Use SavedStateHandle for small transient state or a local database like Room (Step 8) for data that must survive process death. See our ViewModel State Management guide for the full breakdown.
What is the difference between viewModels() and activityViewModels()?
viewModels() scopes the ViewModel to the Fragment — created fresh for each Fragment, destroyed when the Fragment is destroyed. activityViewModels() scopes it to the host Activity — all Fragments in that Activity share the exact same instance, enabling data sharing between Fragments without direct coupling.
Should I use LiveData or StateFlow?
For beginners: LiveData — lifecycle awareness is automatic, no extra setup. For Kotlin-first projects using Coroutines or Compose: StateFlow — it's the modern recommendation and integrates more naturally with coroutines and collectAsStateWithLifecycle().
Why use viewLifecycleOwner instead of this in a Fragment?
A Fragment instance can outlive its View (e.g. on the back stack). Using this as the lifecycle owner keeps the observer active after the View is gone. Each time the Fragment re-appears, a new observer stacks on top — the callback fires multiple times per update and you have a memory leak. viewLifecycleOwner ties the observer to the View's lifecycle, so it's cleaned up properly in onDestroyView().
- ← Step 6: Mastering RecyclerView — Efficient List Rendering
- Step 8: Room Database - Permanent Local Storage →
- Series Hub: Learn Android with Kotlin — Full Roadmap
- Related: ViewModel State Management — LiveData, StateFlow & SavedStateHandle
- Related: Understanding ViewModel Lifecycle in Android
- Related: Kotlin Coroutines for Android — Suspend Functions, Scopes and Dispatchers
- Related: Android ViewModel Example — Build Your First ViewModel
- ViewModel survives configuration changes — rotation, keyboard, language, dark mode
- Use
by viewModels()in Activity;by activityViewModels()to share between Fragments - Keep
MutableLiveDataprivate — expose read-onlyLiveDatato the UI - Always use
viewLifecycleOwner(notthis) when observing in a Fragment - Use
viewModelScopefor coroutines — auto-cancelled when ViewModel is cleared - ViewModel does NOT survive process death — use
SavedStateHandleor Room (Step 8) for that - LiveData for beginners; StateFlow for Kotlin-first and Compose projects
- Never store a Context or View reference in ViewModel — causes memory leaks