💾 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
MVVM layers
| Layer | करता है | कभी नहीं करता |
|---|---|---|
| View (Fragment) | Data दिखाता, clicks ViewModel को भेजता | SQL, network calls |
| ViewModel | UI state रखता, viewModelScope में काम | Views या Activity का Context छूना |
| Repository / Model | Data पढ़ना और लिखना | 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)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
}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।
MutableLiveDatapublic रखना → कोई भी 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 होता है।
