Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
💾 Data, Network और Architecture  ·  Lesson 31

MVVM: ViewModel, LiveData और Flow

MVVM कौन सी problem हल करता है

सब कुछ Activity में

  • Rotation पर data खो जाता है
  • Database code UI code में मिला
  • Test करना मुश्किल
  • 1000 lines की बड़ी files

MVVM

  • ViewModel rotation में बचा रहता है
  • साफ़ layers
  • Logic बिना phone के test
  • छोटी focused classes
हर modern Android app MVVM क्यों use करती है

MVVM layers

ViewActivity / FragmentViewModelstate + actionsRepositorysource तय करताModelRoom · Retrofit
Arrows एक तरफ़; data LiveData / Flow से वापस आता है
Layerकरता हैकभी नहीं करता
View (Fragment)Data दिखाता, clicks ViewModel को भेजताSQL, network calls
ViewModelUI state रखता, viewModelScope में कामViews या Activity का Context छूना
Repository / ModelData पढ़ना और लिखनाScreens के बारे में जानना

पहला 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 (पहली)ViewModelActivity (rotation के बाद)increment() ×3 → count = 3rotate → destroyedby viewModels() → वही instanceobserve → 3 दिखाता
ViewModel Activity instance से ज़्यादा जीता है

LiveData

LiveData एक observable value है जो lifecycles जानती है: सिर्फ़ started observers को updates भेजती है, और destroy होने पर उन्हें अपने आप हटा देती है। LiveData expose करें, MutableLiveData private रखें। Room Flow को .asLiveData() से बदलें।

StateFlow और 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) }
    }
}

Sealed class से UI state

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, या Retry के साथ Error

Student Diary का ViewModel

एक shared ViewModel सारी screens संभालता है: search वाली lists, Progress screen के counts, reminders के साथ save/delete, और 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()

आम गलतियाँ

  • NoteViewModel() से ViewModel बनाना → हर बार नया instance; हमेशा by viewModels() use करें।
  • Fragment का binding या Context ViewModel में रखना → memory leak।
  • MutableLiveData public रखना → कोई भी screen state बदल सकती है; read-only types expose करें।
  • repeatOnLifecycle के बिना lifecycleScope.launch में Flow collect → background में चलता रहता है।

सारांश

  • MVVM = View ↔ ViewModel ↔ Repository ↔ Model।
  • ViewModel rotation में बचता है; काम viewModelScope में चलता है।
  • LiveData/StateFlow expose करें; screen states sealed class से बनाएँ।
✅ Practice

Expense list का logic live update होने वाले total के साथ ExpenseViewModel में ले जाइए, और Loading/Empty/Data के लिए sealed UiState जोड़िए।

अक्सर पूछे जाने वाले प्रश्न (FAQ)

क्या ViewModel में View या Activity का reference रखें?
कभी नहीं। ViewModel views से ज़्यादा जीता है; उन्हें रखने से memory leak होता है। Data expose करें और UI को observe करने दें। Application context चाहिए तो AndroidViewModel use करें।
LiveData या StateFlow?
दोनों चलते हैं। LiveData simple है और default रूप से lifecycle-aware। StateFlow pure Kotlin है, ज़्यादा operators हैं और Room Flow के साथ अच्छा चलता है; इसे repeatOnLifecycle से collect करें।
ViewModel कब destroy होता है?
जब उसका owner पूरी तरह खत्म हो: Activity finish हो या Fragment हटे — rotation पर नहीं। onCleared() call होता है और viewModelScope cancel होता है।
← Android Tutorial (Kotlin)
🔗

Share this topic with a friend

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

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

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

अगला topicAndroid में Kotlin Coroutines आपकी प्रगति: 31 / 43 (72%) · Lesson 32 / 43 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें