Step 7: LiveData and ViewModel — Smarter Data Handling

📚 Learn Android with Kotlin — Series
1. Kotlin 2. Android Studio 3. Activities 4. Intents 5. Fragments 6. RecyclerView 7. LiveData 8. Room 9. Retrofit 10. Material 🔒 11. Firebase 🔒 12. Testing 🔒 13. Publish 🔒

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.

Android ViewModel and LiveData — lifecycle-aware data management

ViewModel — your data's safe house across the full Android lifecycle

Prerequisites: You should be comfortable with Fragments (Step 5) and RecyclerView (Step 6). Knowledge of Kotlin Coroutines will help for the advanced section.

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.

Android ViewModel lifecycle — survives screen rotation

ViewModel lifecycle — created once, survives rotation, destroyed only when Activity is finished


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
        }
    }
}
⚠️ Common mistake: Using 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 AndroidViewModel instead.
  • Expose immutable data to the UIMutableLiveData/MutableStateFlow stays 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 viewModelScope for 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 viewLifecycleOwner in Fragments — without exception. Using this leads 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

  1. 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.
  2. 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 call adapter.submitList() in the observer. Add a "Shuffle" button that shuffles the list in the ViewModel and watch DiffUtil animate the result.
  3. 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.

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().

📚 Continue the Series
📝 Step 7 Summary
  • ViewModel survives configuration changes — rotation, keyboard, language, dark mode
  • Use by viewModels() in Activity; by activityViewModels() to share between Fragments
  • Keep MutableLiveData private — expose read-only LiveData to the UI
  • Always use viewLifecycleOwner (not this) when observing in a Fragment
  • Use viewModelScope for coroutines — auto-cancelled when ViewModel is cleared
  • ViewModel does NOT survive process death — use SavedStateHandle or 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

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

Post a Comment

Please let us know about any concerns or query.

Previous Post Next Post

Contact Form