,

آموزش ViewModel در jetpack Compose

آموزش ViewModel در jetpack Compose

در این مبحث می خوایم به پیاده‌سازی ViewModel در compose UI بپردازیم.

قبلش یه مروری کنیم به مطلب جلسه قبلمون که راجع State ها بود و بعد بریم سراغ ویو مدل:

۱. درک State (حالت) در جت‌پک کامپوز

در جت‌پک کامپوز، State به هر مقداری گفته می‌شه که وقتی تغییر می‌کنه، باعث می‌شه رابط کاربری (UI) دوباره رسم بشه(ریکامپوز). به عنوان مثال، متن داخل یک فیلد، یا وضعیت فعال یا غیرفعال بودن یک دکمه، همگی State هستند.

remember و mutableStateOf

برای اینکه کامپوز از تغییرات یک متغیر مطلع بشه و UI رو به‌روز کنه، باید از توابع خاصی استفاده کرد. تابع mutableStateOf یک شیء قابل تغییر رو برمی‌گردونه و تابع remember این شیء رو در حافظه نگه می‌داره تا با هر بار اجرای مجدد کامپوزبل، مقدارش از بین نره.

// یک مثال ساده از استفاده از remember
@Composable
fun Counter() {
    val count by remember { mutableStateOf(0) } // مقدار count در طول بازسازی کامپوزبل حفظ می‌شه
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

rememberSaveable

rememberSaveable شبیه به remember عمل می‌کنه، اما یک مزیت مهم داره: مقدار رو در برابر تغییرات پیکربندی (مثل چرخش صفحه) و حتی قطع شدن فرآیند برنامه (Process Death) حفظ می‌کنه.

چرا این توابع کافی نیستند؟

با اینکه remember و rememberSaveable ابزارهای قدرتمندی هستند، اما برای مدیریت تمامی حالت‌های برنامه کافی نیستند. اصلی‌ترین محدودیت اون‌ها اینه که فقط در محدوده‌ی کامپوزبل‌هایی که در حال حاضر روی صفحه نمایش داده می‌شن، کار می‌کنند. به محض اینکه یک کامپوزبل از ترکیب UI خارج بشه، داده‌های remember و rememberSaveable از بین می‌رن. همچنین، این توابع برای نگهداری منطق های پیچیده‌ی مثل اتصال به دیتابیس و صدا زدن api مناسب نیستند و باعث شلوغی و سخت‌شدن مدیریت کد می‌شن.


۲. معرفی ViewModel: راه‌حل مدیریت داده

viewmodel در jetpack compose
viewmodel در jetpack compose

ViewModel یک کلاس است که برای نگهداری و مدیریت داده‌های مرتبط با UI به شکلی پایدار و جدا از چرخه عمر View طراحی شده.

برخلاف ریممبر، ViewModel داده‌ها رو حتی در طول تغییرات پیکربندی (مثل چرخش صفحه، تغییر زبان و …) نگه می‌داره. ViewModel توسط اندروید مدیریت می‌شه و تنها زمانی که Activity مربوطه برای همیشه نابود بشه، از بین می‌ره.

مزایای استفاده از ViewModel:

  • جداسازی منطق از UI: کد شما خواناتر و قابل نگهداری‌تر می‌شه.
  • مقاومت در برابر تغییرات پیکربندی: داده‌های شما در برابر چرخش صفحه حفظ می‌شن.
  • قابلیت تست‌پذیری: منطق شما (ViewModel) بدون نیاز به UI قابل تست هست.
  • مدیریت چرخه عمر (LifeCycle): شما نگرانی بابت مدیریت داده‌ها در طول تغییرات چرخه عمر ندارید.

۳. StateFlow در مقابل MutableState

ما می تونیم در ویو مدل، هم از StateFlow و هم از mutableStateOf استفاده کنیم. هر دوی این‌ها برای مدیریت State هستند اما تفاوت‌هایی دارند:

  • mutableStateOf: این یک نوع از State مخصوص کامپوز هست. وقتی مقدارش تغییر می‌کنه، کامپوزبل‌هایی که ازش استفاده می‌کنند رو بلافاصله به‌روز می‌کنه. برای مدیریت حالت‌های ساده‌ی UI در خود کامپوزبل‌ها مناسبه.
  • StateFlow: این یک نوع جریان (Flow) از کتابخانه‌ی Kotlin Coroutines هست. StateFlow برای نگهداری و ارسال آخرین مقدار یک حالت به چندین دریافت‌کننده (Collecter) به صورت بهینه‌تر طراحی شده. از اونجایی که StateFlow از ViewModel می‌اد، بهترین انتخاب برای مدیریت داده‌هایی هست که از لایه‌های پایین‌تر (مثل لایه‌ی داده یا شبکه) می‌رسند.

به طور خلاصه، برای حالت‌های ساده‌ی داخلی کامپوزبل از mutableStateOf و برای مدیریت حالت‌های پیچیده‌تر و جریان‌های داده در ViewModel از StateFlow استفاده کنید.


۴.پیاده‌سازی یک مثال ساده:

گام ۱: اضافه کردن وابستگی‌های لازم

اطمینان حاصل کنید که وابستگی‌های (dependencies) زیر در فایل build.gradle.kts یا build.gradle (Module: app) شما وجود دارند. . (بررسی آخرین نسخه)

Gradle

// build.gradle.kts (Module: app)
dependencies {
    // برای استفاده از ViewModel در کامپوز
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2")
}

این یک مثال ساده از پیاده‌سازی ViewModel در Jetpack Compose اندروید هست، شامل نمایش یک شمارنده که با کلیک بر روی یک دکمه افزایش می‌یابد.

در این مثال، ViewModel یک متغیر mutableStateOf به نام count دارد که مقدار آن با هر بار فراخوانی تابع incrementCount یک واحد افزایش می‌یابد.

class CounterViewModel : ViewModel() {
    var count by mutableStateOf(0)
    fun incrementCount() {
        count++
    }
}

در این بخش، کامپوزابل CounterScreen از ViewModel برای نمایش و به‌روزرسانی مقدار شمارنده استفاده می‌کند. تابع viewModel() به صورت خودکار یک نمونه از CounterViewModel را ایجاد یا بازیابی می‌کند. هر زمان که مقدار count در ViewModel تغییر کند، کامپوزابل به صورت خودکار بروز (recompose) می‌شود.

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
 
    val currentCount = viewModel.count

    Column(
        modifier = Modifier.fillMaxSize(),
    ) {
        // نمایش مقدار شمارنده
        Text(text = "Count: $currentCount")

        // دکمه‌ای برای افزایش شمارنده
        Button(onClick = { viewModel.incrementCount() }) {
            Text(text = "Increment")
        }
    }
}

۵. پیاده‌سازی گام‌به‌گام مثال گفته شده در ویدیو:

گام ۱ : شبیه مثال قبل

گام ۲: ایجاد و تعریف ViewModel

یک فایل جدید به نام LoginScreenVM.kt در مسیر مناسب (مثلاً com.your.app.ui.screen.loginScreen) ایجاد کنید و کدهای مربوط به ViewModel را درون آن قرار دهید:

Kotlin

// LoginScreenVM.kt
package com.esfandune.esfadnune_v1.ui.screen.loginScreen

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class LoginScreenVM : ViewModel() {
    // استفاده از StateFlow برای ایمیل (مناسب برای داده‌های پیچیده یا async)
    private val _email = MutableStateFlow("")
    val email = _email.asStateFlow()

    // استفاده از mutableStateOf برای رمز عبور (مناسب برای حالت‌های ساده)
    var password by mutableStateOf("")

    fun updateEmail(newEmail: String) {
        _email.value = newEmail
    }

    fun login(onLoginSuccess: () -> Unit, onLoginError: (String) -> Unit) {
        if (email.value.isNotEmpty() && password.isNotEmpty()) {
            onLoginSuccess()
        } else {
            onLoginError("نام کاربری و کلمه عبور را وارد نمایید.")
        }
    }
}

گام ۳: ایجاد و پیاده‌سازی Composable UI

یک فایل جدید به نام LoginScreen.kt در همان پکیج ایجاد کرده و کدهای رابط کاربری را درون آن قرار دهید.

Kotlin

// LoginScreen.kt
package com.esfandune.esfadnune_v1.ui.screen.loginScreen

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
    modifier: Modifier = Modifier,
    // ViewModel به عنوان پارامتر تزریق می‌شود
    loginScreenVM: LoginScreenVM = viewModel()
) {
    Column(
        modifier = modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "ورود به حساب کاربری")
        Spacer(modifier = Modifier.height(16.dp))

        OutlinedTextField(
            // مقدار ایمیل رو از ViewModel می‌خونه
            value = loginScreenVM.email.collectAsState().value,
            // تغییرات رو به ViewModel می‌فرسته
            onValueChange = { loginScreenVM.updateEmail(it) },
            label = { Text("ایمیل") }
        )

        Spacer(modifier = Modifier.height(8.dp))

        OutlinedTextField(
            // مقدار رمز عبور رو از ViewModel می‌خونه
            value = loginScreenVM.password,
            // تغییرات رو به ViewModel می‌فرسته
            onValueChange = { loginScreenVM.password = it },
            label = { Text("رمز عبور") },
            visualTransformation = PasswordVisualTransformation()
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = {
            // تابع login در ViewModel فراخوانی می‌شه
            loginScreenVM.login(onLoginSuccess = {}, onLoginError = {})
        }) {
            Text("ورود")
        }
    }
}

گام ۴: چگونگی کارکرد نهایی

  1. ایجاد ViewModel: در تابع LoginScreen، تابع viewModel() یک نمونه از LoginScreenVM ایجاد می‌کنه. اگر صفحه بچرخه، کامپوزبل دوباره ساخته می‌شه، اما viewModel() همان نمونه‌ی قبلی را برمی‌گرداند.
  2. اتصال UI به State:
    • OutlinedTextField برای ایمیل، با استفاده از collectAsState() به StateFlow ایمیل در ViewModel متصل می‌شه. هر تغییری در ایمیل، UI رو به‌روز می‌کنه.
    • OutlinedTextField برای رمز عبور، به mutableStateOf رمز عبور متصل می‌شه و با هر تغییر، UI هم به‌روزرسانی می‌شه.
  3. فراخوانی منطق: با کلیک روی دکمه، تابع login() در ViewModel فراخوانی می‌شه. این تابع منطق برنامه (بررسی خالی نبودن فیلدها) رو انجام می‌ده.

به این ترتیب، UI فقط به وظیفه نمایش داده‌ها و ارسال رویدادها (مثل کلیک) به ViewModel می‌پردازه و ViewModel تمام منطق رو مدیریت می‌کنه. این جداسازی باعث می‌شه که کد شما ساختار یافته و قابل نگهداری باشه.

Comments

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *