Məzmuna keç
Educora
İrəli22 dəq11 / 14

Vəziyyət və arxitektura: ViewModel və MVVM

Compose-da vəziyyəti `remember` və `mutableStateOf` ilə saxla, onu yuxarı qaldır (state hoisting), ekranın məlumatını ViewModel və StateFlow-a köçür və tətbiqi birtərəfli məlumat axını ilə MVVM üslubunda qur.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • remember, rememberSaveable və mutableStateOf-un fərqini izah etmək
  • Vəziyyəti yuxarı qaldırıb vəziyyətsiz (stateless) composable yazmaq
  • ViewModel-də StateFlow ilə ekran vəziyyətini saxlamaq və Compose-da oxumaq
  • Birtərəfli məlumat axınını və MVVM qatlarını təsvir etmək

Leyla viktorina tətbiqində üçüncü suala çatıb və telefonu yan çevirir — hesab sıfırlanır, o isə yenidən başlamalı olur. Belə səhvlərin hamısının kökü eynidir: vəziyyət (state) düzgün yerdə saxlanmayıb. Vəziyyət zamanla dəyişə bilən və ekrana təsir edən istənilən qiymətdir: yazılan mətn, açarın vəziyyəti, yüklənən siyahı, hesab. Bu dərsdə onu harada və necə saxlamağı öyrənəcəksən.

remember və mutableStateOf

Compose yalnız müşahidə olunan vəziyyətin dəyişməsini görür. mutableStateOf(...) belə qiymət yaradır: onu dəyişəndə Compose həmin qiyməti oxuyan funksiyaları yenidən çağırır (recomposition). remember { } isə qiyməti yenidən kompozisiyalar arasında yadda saxlayır; onsuz funksiya hər çağırışda sıfırdan başlayardı. by açar sözü .value yazmadan işləməyə imkan verir.

Səhv: adi dəyişən
@Composable
fun Counter() {
    var count = 0
    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}
Düzgün: yadda saxlanan vəziyyət
@Composable
fun Counter() {
    var count by rememberSaveable { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}
Solda count artır, amma Compose bundan xəbər tutmur və düymədə həmişə 0 qalır. Sağda hər toxunuş ekranı yeniləyir, qiymət isə ekran döndəriləndə də qorunur.

Vəziyyəti yuxarı qaldırmaq

Tərif
Vəziyyətin yuxarı qaldırılması (state hoisting)

Vəziyyəti composable-ın içindən onu çağıran funksiyaya köçürmək. Composable qiyməti parametr kimi alır, dəyişiklik istəyini isə lambda ilə yuxarı bildirir. Belə funksiya vəziyyətsiz (stateless) olur: onu yoxlamaq, önizləmək və təkrar istifadə etmək asandır.

Kotlin
@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableIntStateOf(0) }
    Counter(count = count, onIncrement = { count++ })
}

@Composable
fun Counter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
    Button(onClick = onIncrement, modifier = modifier) {
        Text("Clicked $count times")
    }
}
CounterScreen vəziyyətin sahibidir, Counter isə sadəcə onu göstərir və toxunuşu bildirir.

ViewModel və StateFlow

ViewModel ekranın məlumatını və məntiqini saxlayan sinifdir. O, ekran döndəriləndə yenidən yaradılmır, ona görə hesab itmir. Vəziyyəti xaricə StateFlow ilə veririk — bu, həmişə cari qiyməti olan və dəyişiklikləri yayımlayan axındır. İçəridə dəyişə bilən MutableStateFlow gizlədilir, xaricə isə yalnız oxunan StateFlow açılır.

Kotlin
data class QuizUiState(
    val question: Int = 1,
    val score: Int = 0,
    val finished: Boolean = false
)

class QuizViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(QuizUiState())
    val uiState: StateFlow<QuizUiState> = _uiState.asStateFlow()

    fun answer(correct: Boolean) {
        _uiState.update { state ->
            val next = state.question + 1
            state.copy(
                question = next,
                score = if (correct) state.score + 1 else state.score,
                finished = next > 5
            )
        }
    }
}
Bütün ekran vəziyyəti bir dəyişməz data class-dadır; update onu atomar şəkildə yeni surətlə əvəz edir.
Kotlin
@Composable
fun QuizScreen(viewModel: QuizViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    Column(modifier = Modifier.padding(16.dp)) {
        if (state.finished) {
            Text("Your score: ${state.score} / 5")
        } else {
            Text("Question ${state.question} of 5")
            Button(onClick = { viewModel.answer(correct = true) }) { Text("Correct") }
            Button(onClick = { viewModel.answer(correct = false) }) { Text("Wrong") }
        }
    }
}
Ekranda: «Question 1 of 5» və iki düymə; beşinci cavabdan sonra «Your score: 3 / 5» kimi nəticə. Ekranı döndərmək nəticəyə təsir etmir.

viewModel() funksiyası ekran üçün ViewModel-i yaradır və ya mövcud olanı qaytarır. collectAsStateWithLifecycle() axını Compose vəziyyətinə çevirir və tətbiq arxa planda olanda toplamanı dayandırır. Bu funksiyalar lifecycle-viewmodel-compose və lifecycle-runtime-compose kitabxanalarındadır.

  1. 1
    Kataloqa əlavə et

    gradle/libs.versions.toml faylında [libraries] bölməsinə iki sətir yaz:
    androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
    androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }

  2. 2
    Modula qoş

    app/build.gradle.kts-də dependencies blokuna implementation(libs.androidx.lifecycle.viewmodel.compose) və implementation(libs.androidx.lifecycle.runtime.compose) əlavə et.

  3. 3
    Sinxronlaşdır və import et

    Sync Now bas, sonra qırmızı görünən viewModel və collectAsStateWithLifecycle üzərində Alt+Enter basıb import seç.

Birtərəfli axın və MVVM

Yuxarıdakı kodda məlumat bir dairə üzrə hərəkət edir: ekran vəziyyəti göstərir → istifadəçi düyməyə basır → ekran ViewModel-ə hadisə göndərir (answer) → ViewModel yeni vəziyyət yaradır → ekran onu göstərir. Buna birtərəfli məlumat axını (UDF) deyilir. Vəziyyəti yalnız bir yer dəyişdiyi üçün səhvləri tapmaq asanlaşır. Bu məntiqi adi Kotlin ilə də yoxlamaq olar:

Kotlin
data class CounterState(val count: Int = 0, val message: String = "")

sealed interface CounterEvent {
    data object Increment : CounterEvent
    data object Reset : CounterEvent
}

fun reduce(state: CounterState, event: CounterEvent): CounterState = when (event) {
    CounterEvent.Increment -> state.copy(
        count = state.count + 1,
        message = if (state.count + 1 >= 3) "Great streak!" else ""
    )
    CounterEvent.Reset -> CounterState()
}

fun main() {
    var state = CounterState()
    val events = listOf(CounterEvent.Increment, CounterEvent.Increment, CounterEvent.Increment, CounterEvent.Reset)
    for (event in events) {
        state = reduce(state, event)
        println("$event -> $state")
    }
}
Gözlənilən nəticə
Increment -> CounterState(count=1, message=)
Increment -> CounterState(count=2, message=)
Increment -> CounterState(count=3, message=Great streak!)
Reset -> CounterState(count=0, message=)
MVVM qatıVəzifəsiNümunə
Model (məlumat)məlumatı internetdən və ya verilənlər bazasından alır və saxlayırLessonRepository
ViewModelekran vəziyyətini saxlayır, hadisələri emal edirQuizViewModel
View (görünüş)vəziyyəti göstərir, hadisələri ViewModel-ə ötürürQuizScreen
ViewModel heç vaxt composable-ları və ya Context-i birbaşa saxlamamalıdır.
seçilmiş kodu ayrıca funksiyaya çıxarmaq — Extract Function (Mac: Cmd+Option+M)Ctrl+Alt+M
adı bütün layihədə dəyişmək — RenameShift+F6
funksiyanın harada çağırıldığını tapmaq — Find Usages (Mac: Option+F7)Alt+F7
faylın quruluşu: siniflər və funksiyalar siyahısı (Mac: Cmd+F12)Ctrl+F12

Əsas fikirlər

  • Compose yalnız mutableStateOf kimi müşahidə olunan vəziyyətin dəyişməsini görür; remember onu yenidən kompozisiyalar arasında saxlayır.
  • rememberSaveable sadə qiymətləri ekran döndəriləndə də qoruyur; ekranın əsas məlumatı ViewModel-də saxlanır.
  • Vəziyyəti qaldır: value aşağı enir, onValueChange kimi hadisələr yuxarı qalxır.
  • ViewModel vəziyyəti gizli MutableStateFlow-da saxlayır və açıq StateFlow kimi verir; ekran onu collectAsStateWithLifecycle() ilə oxuyur.
  • MVVM: Model məlumatı verir, ViewModel vəziyyəti idarə edir, View onu göstərir — məlumat bir istiqamətdə hərəkət edir.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
Ekran döndəriləndə remember { mutableStateOf(0) } ilə saxlanan qiymətə nə olur?