در این مبحث می خوایم به پیادهسازی 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 یک کلاس است که برای نگهداری و مدیریت دادههای مرتبط با 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("ورود")
}
}
}
گام ۴: چگونگی کارکرد نهایی
- ایجاد ViewModel: در تابع
LoginScreen
، تابعviewModel()
یک نمونه ازLoginScreenVM
ایجاد میکنه. اگر صفحه بچرخه، کامپوزبل دوباره ساخته میشه، اماviewModel()
همان نمونهی قبلی را برمیگرداند. - اتصال UI به State:
OutlinedTextField
برای ایمیل، با استفاده ازcollectAsState()
بهStateFlow
ایمیل در ViewModel متصل میشه. هر تغییری در ایمیل، UI رو بهروز میکنه.OutlinedTextField
برای رمز عبور، بهmutableStateOf
رمز عبور متصل میشه و با هر تغییر، UI هم بهروزرسانی میشه.
- فراخوانی منطق: با کلیک روی دکمه، تابع
login()
در ViewModel فراخوانی میشه. این تابع منطق برنامه (بررسی خالی نبودن فیلدها) رو انجام میده.
به این ترتیب، UI فقط به وظیفه نمایش دادهها و ارسال رویدادها (مثل کلیک) به ViewModel میپردازه و ViewModel تمام منطق رو مدیریت میکنه. این جداسازی باعث میشه که کد شما ساختار یافته و قابل نگهداری باشه.
دیدگاهتان را بنویسید