Kódu, ktorý píšeme okolo nášho UI sa začína množiť. Zrazu potrebujeme nejaké CoroutineScope, voláme HTTP requesty
a parsujeme JSONy priamo vedľa vykresľovania tlačidla. Akonáhle pridáme trocho viac aplikačnej logiky, náš projekt
bude kopa sraček.
Existujú viaceré metódy, akými vieme organizovať aplikačnú logiku, konkrétne:
Všetky 4 majú spoločné písmenka M ako Model (manipulácia s dátami) a V ako View (UI). Zvyšné písmenka popisujú mierne
odlišné metódy komunikácie medzi Modelom a Viewom. Z týchto sa v Androide používajú výlučne MVVM a MVI. Ja vám
ukážem MVVM, ale MVI sa v praxi používa častejšie, lebo je podstatne prehľadnejší pre čítanie.
Centrom MVVM aj MVI je trieda ViewModel. Keď UI potrebuje získať dáta, alebo vykonať nejakú akciu, ktorá
priamo nesúvisí s UI, zavoláme jednoducho funkciu z ViewModel - čo je len jednoduchá samostatná trieda, ktorá prežije
aj configuration changes napríklad zmenu orientácie telefónu, či zmenu jazyka. Disponuje navyše vlastným
CoroutineScope, takže už viac nebudeme potrebovať volať rememberCoroutineScope v UI.
Závislosti potrebujeme nasledovné:
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
}
Vo ViewModel-e môžeme definovať funkcie, ktoré budú volané z UI. Aby sme UI notifikovali o zmenách, budeme potrebovať
pracovať s MutableStateFlow-om, na ktorý vie UI počúvať. Nikdy aktívne neposielame dáta z ViewModelu do UI.
// MV(ViewModel)
class DashboardViewModel : ViewModel() {
val apiState = MutableStateFlow("idle") // state pocuvatelny na UI
fun addTodo(todo: AddTodoRequest) {
viewModelScope.launch { // kazdy viewmodel ma vlastny CoroutineScope - nemusime ho vytvarat
apiState.emit("loading") // zmen stav na loading
// do some work
apiState.emit("success") // zmen stav na success
}
}
}
A následne v UI
ViewModelViewModel vyvolá// M(View)VM
setContent {
val viewModel = viewModel<DashboardViewModel>() // (1) ziskanie viewmodelu
val apiState by viewModel.apiState.collectAsState() // (3) pocuvam na zmenu premennej apiState
Button(onClick = {
viewModel.addTodo(AddTodoRequest("New todo", false, 1)) // (2) volam funkciu addTodo z ViewModelu
}) { Text(apiState) } // (4) zobrazujem aktualny stav apiState
}
Ako môžeš vidieť. View časť sa značne zjednoduší a sprehľadní. Všetku aplikačnú logiku sme odsunuli do ViewModelu.
Model-View-Intent tiež pracuje s triedou ViewModel, ale iným princípom:
addTodo vrátiť aj nejaký výsledok. V MVI to je zakázané - UI sa updatuje vždy výlučne počúvaním zmien v MutableStateFlowsendIntent. Tá prijíma takzvaný Intent, často sealed trieduPrikladám jednoduchú ukážku ViewModelu v architektúre MVI:
sealed class DashboardIntent {
data class AddTodo(val todo: AddTodoRequest) : DashboardIntent()
object Refresh : DashboardIntent()
}
class DashboardViewModel : ViewModel() {
fun sendIntent(intent: DashboardIntent) {
when (intent) {
is DashboardIntent.AddTodo -> addTodo(intent.todo)
DashboardIntent.Refresh -> refresh()
}
}
private fun addTodo(todo: AddTodoRequest) { }
private fun refresh() { }
}
V MVI máme radi použitie jediného objektu, ktorý drží komplet stav celej obrazovky, na ktorú je ViewModel
naviazaný. Preto často uvidíš len jediný MutableStateFlow v danom ViewModeli.
V MVVM aj MVI niekedy potrebujeme riešiť one-shot akcie, ktoré sa majú vykonať na UI. Tu sa nehodí používať
MutableStateFlow-y, lebo ich úlohou je držať state (vidíš slovíčko State v MutableStateFlow?). Pre takéto
one-shot akcie používame Channel-y:
sealed interface DashboardEvent {
data object OnTodoAdded : DashboardEvent
}
class DashboardViewModel : ViewModel() {
private val _events = Channel<DashboardEvent>()
val events = _events.receiveAsFlow()
private fun addTodo(todo: AddTodoRequest) {
viewModelScope.launch {
_events.send(DashboardEvent.OnTodoAdded)
}
}
}
Použitie vo View je nasledovné:
setContent {
val viewModel = viewModel<DashboardViewModel>()
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
when (event) {
is DashboardEvent.OnTodoAdded -> {
// tu mozem aktualizovat nejaky state...
}
}
}
}
}
}
Použitý je tu takzvaný LaunchedEffect, ktorý vysvetlím až v téme 18.1 LaunchedEffect.