Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
💾 Data, Network & Architecture  ·  Lesson 31

MVVM: ViewModel, LiveData & Flow

The problem MVVM solves

Everything in the Activity

  • Data lost on rotation
  • Database code mixed with UI code
  • Hard to test
  • Huge 1000-line files

MVVM

  • ViewModel survives rotation
  • Clear layers
  • Logic testable without a phone
  • Small focused classes
Why every modern Android app uses MVVM

The MVVM layers

ViewActivity / FragmentViewModelstate + actionsRepositorydecides sourceModelRoom · Retrofit
Arrows go one way; data comes back as LiveData / Flow
LayerDoesNever does
View (Fragment)Shows data, sends clicks to the ViewModelSQL, network calls
ViewModelHolds UI state, runs work in viewModelScopeTouches Views or Context of an Activity
Repository / ModelReads and writes dataKnows about screens

A first ViewModel

Kotlin
// CounterViewModel.kt
class CounterViewModel : ViewModel() {
    private val _count = MutableLiveData(0)        // private, changeable
    val count: LiveData<Int> = _count              // public, read-only

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

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val viewModel: CounterViewModel by viewModels()     // activity-ktx

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        viewModel.count.observe(this) { binding.tvCount.text = it.toString() }
        binding.btnPlus.setOnClickListener { viewModel.increment() }
    }
}
// Rotate the phone: the count stays! (no onSaveInstanceState needed)
Activity (1st)ViewModelActivity (after rotation)increment() ×3 → count = 3rotate → destroyedby viewModels() → SAME instanceobserve → shows 3
The ViewModel outlives the Activity instance

LiveData

LiveData is an observable value that knows about lifecycles: it only sends updates to observers that are started, and removes them automatically when destroyed. Expose LiveData, keep MutableLiveData private. Convert a Room Flow with .asLiveData().

StateFlow and Flow

Kotlin
class SearchViewModel(private val repo: NoteRepository) : ViewModel() {
    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query

    // a Flow that always shows the notes matching the latest query
    @OptIn(ExperimentalCoroutinesApi::class)
    val results: StateFlow<List<Note>> = _query
        .debounce(300)                                   // wait until the user stops typing
        .flatMapLatest { q -> if (q.isBlank()) repo.allNotes else repo.search(q) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

    fun onQueryChanged(q: String) { _query.value = q }
}

// Fragment – collect safely
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.results.collect { adapter.submitList(it) }
    }
}

UI state with a sealed class

Kotlin
sealed class TipState {
    data object Loading : TipState()
    data class Success(val tip: Tip) : TipState()
    data class Error(val message: String) : TipState()
}

// Fragment: one observer draws every state
viewModel.tip.observe(viewLifecycleOwner) { state ->
    binding.tipProgress.isVisible = state is TipState.Loading
    binding.tipContent.isVisible = state is TipState.Success
    binding.tipError.isVisible = state is TipState.Error
    if (state is TipState.Success) binding.tvTipTitle.text = state.tip.title
}
9:41▮▮ 85%Progress⋮Tip of the day⏳ Loading…📝Notes📚Subjects📊Progress9:41▮▮ 85%Progress⋮🖼️tip image (Glide)Use View BindingReplace findViewById…📝Notes📚Subjects📊Progress9:41▮▮ 85%Progress⋮Could not load the tip.Retry📝Notes📚Subjects📊Progress
Loading → Success, or Error with Retry

The Student Diary ViewModel

One shared ViewModel serves all screens: lists with search, counts for the Progress screen, save/delete with reminders, and the network tip.

Kotlin – ui/NoteViewModel.kt (complete)
package com.codingeasily.studentdiary.ui

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.codingeasily.studentdiary.data.AppDatabase
import com.codingeasily.studentdiary.data.Note
import com.codingeasily.studentdiary.data.NoteRepository
import com.codingeasily.studentdiary.data.SubjectCount
import com.codingeasily.studentdiary.network.Tip
import com.codingeasily.studentdiary.network.TipsApi
import com.codingeasily.studentdiary.reminder.ReminderScheduler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

/** Which notes a list screen shows. */
enum class NoteFilter { ALL, PENDING, DONE, FAVORITES, SUBJECT }

/** State of the "Tip of the day" card. */
sealed class TipState {
    data object Loading : TipState()
    data class Success(val tip: Tip) : TipState()
    data class Error(val message: String) : TipState()
}

/**
 * Shared by all screens (activityViewModels). Survives screen rotation.
 * The UI observes LiveData; all database work runs in viewModelScope.
 */
class NoteViewModel(application: Application) : AndroidViewModel(application) {

    private val repo = NoteRepository(AppDatabase.get(application).noteDao())

    // ---------- lists ----------
    private val searchQuery = MutableStateFlow("")

    fun setSearchQuery(q: String) { searchQuery.value = q.trim() }

    @OptIn(ExperimentalCoroutinesApi::class)
    fun notes(filter: NoteFilter, subject: String? = null): LiveData<List<Note>> {
        val source: Flow<List<Note>> = when (filter) {
            NoteFilter.ALL -> repo.allNotes
            NoteFilter.PENDING -> repo.pendingNotes
            NoteFilter.DONE -> repo.doneNotes
            NoteFilter.FAVORITES -> repo.favorites
            NoteFilter.SUBJECT -> repo.bySubject(subject ?: "")
        }
        return searchQuery.flatMapLatest { q ->
            if (q.isEmpty()) source
            else source.map { list ->
                list.filter { it.title.contains(q, ignoreCase = true) || it.content.contains(q, ignoreCase = true) }
            }
        }.asLiveData()
    }

    val subjectCounts: LiveData<List<SubjectCount>> = repo.subjectCounts.asLiveData()
    val countAll: LiveData<Int> = repo.countAll.asLiveData()
    val countDone: LiveData<Int> = repo.countDone.asLiveData()

    // ---------- single note ----------
    suspend fun getNote(id: Long): Note? = repo.get(id)

    fun save(note: Note, onSaved: (Long) -> Unit) {
        viewModelScope.launch {
            val id = repo.save(note)
            val due = note.dueAt
            if (due != null && !note.isDone) ReminderScheduler.schedule(getApplication(), id, due)
            else ReminderScheduler.cancel(getApplication(), id)
            onSaved(id)
        }
    }

    fun toggleDone(note: Note) = viewModelScope.launch {
        repo.save(note.copy(isDone = !note.isDone))
        if (!note.isDone) ReminderScheduler.cancel(getApplication(), note.id)
    }

    fun toggleFavorite(note: Note) = viewModelScope.launch {
        repo.save(note.copy(isFavorite = !note.isFavorite))
    }

    fun delete(note: Note) = viewModelScope.launch {
        repo.delete(note)
        ReminderScheduler.cancel(getApplication(), note.id)
    }

    /** Used by the "Undo" button of the Snackbar. */
    fun restore(note: Note) = viewModelScope.launch {
        repo.save(note.copy(id = 0))
    }

    fun deleteDone(onDone: (Int) -> Unit) = viewModelScope.launch {
        onDone(repo.deleteDone())
    }

    // ---------- tip of the day (Retrofit) ----------
    private val _tip = MutableLiveData<TipState>()
    val tip: LiveData<TipState> = _tip

    fun loadTip(force: Boolean = false) {
        if (!force && _tip.value is TipState.Success) return
        _tip.value = TipState.Loading
        viewModelScope.launch {
            _tip.value = try {
                val tips = TipsApi.service.getTips().tips
                if (tips.isEmpty()) TipState.Error("No tips yet")
                else TipState.Success(tips[(System.currentTimeMillis() / 86_400_000L % tips.size).toInt()])
            } catch (e: Exception) {
                TipState.Error(e.message ?: "Network error")
            }
        }
    }
}

viewModels() vs activityViewModels()

Kotlin
// Each fragment gets its own instance
private val vm: EditorViewModel by viewModels()

// All fragments of the Activity share ONE instance (Student Diary uses this)
private val viewModel: NoteViewModel by activityViewModels()

Common mistakes

  • Creating a ViewModel with NoteViewModel() → a new instance every time; always use by viewModels().
  • Keeping a Fragment's binding or Context in the ViewModel → memory leak.
  • Exposing MutableLiveData publicly → any screen can change state; expose read-only types.
  • Collecting a Flow in lifecycleScope.launch without repeatOnLifecycle → keeps running in the background.

Summary

  • MVVM = View ↔ ViewModel ↔ Repository ↔ Model.
  • ViewModel survives rotation; work runs in viewModelScope.
  • Expose LiveData/StateFlow; model screen states with a sealed class.
✅ Practice

Move your Expense list logic into an ExpenseViewModel with a total that updates live, and add a sealed UiState for Loading/Empty/Data.

Frequently Asked Questions

Should a ViewModel hold a reference to a View or Activity?
Never. The ViewModel outlives views; holding them causes memory leaks. Expose data and let the UI observe it. Use AndroidViewModel if you need the Application context.
LiveData or StateFlow?
Both work. LiveData is simple and lifecycle-aware out of the box. StateFlow is pure Kotlin, has more operators and fits well with Room Flow; collect it with repeatOnLifecycle.
When is a ViewModel destroyed?
When its owner finishes for good: the Activity is finished or the Fragment is removed — not on rotation. onCleared() is called and viewModelScope is cancelled.
← Android Tutorial (Kotlin)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicKotlin Coroutines in Android Your progress: 31 / 43 (72%) · Lesson 32 / 43 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free