مفهوم بنیادین وضعیت (State) در Jetpack Compose و تفاوت با سیستم سنتی View
در توسعه سنتی اندروید بر پایه View و فایلهای XML، رابط کاربری به عنوان درختی از ویجت های تغییرپذیر (Stateful Widgets) نگه داری می شد. توسعه دهنده مسوولیت فراخوانی دستی متدهایی نظیر setText یا setVisibility را برای به روزرسانی ظاهر صفحه بر عهده داشت.
این مدل دستکاری مستقیم، منشا اصلی خطاهای رایج پروداکشن مانند ناهماهنگی وضعیت بصری با داده های پس زمینه، رقابت های غیرهمگام (Race Conditions) و نشت حافظه بود. با پیدایش جت پک کامپوز، این الگو جای خود را به معماری اعلانی (Declarative) داد.
در کامپوز، رابط کاربری حاصل یک تابع خالص از وضعیت است (UI = f(State)). کامپوننت های کاربری غیرقابل تغییر (Immutable) هستند؛ بدین معنا که با تغییر مقادیر داده ای، تابع کامپوزبل با مقادیر جدید مجددا فراخوانی و بازآفرینی می شود که به این فرآیند اصطلاحا ری کامپوزیشن (Recomposition) می گویند.
هر داده ای که در طول زمان تغییر می کند و تغییر آن باید در صفحه نمایش بازتاب یابد، وضعیت یا State نام دارد. از متن وارد شده در یک فیلد جستجو تا وضعیت بارگذاری اسکلتی (Shimmer)، همگی نمونه هایی از وضعیت در لایه Presentation هستند.
بر اساس بنچمارک های رسمی تیم ابزار های اندروید در گوگل، پیاده سازی الگوی UDF در ترکیب با کامپوز، خطاهای ناهماهنگی وضعیت میان لایه ها را تا ۵۴٪ کاهش داده و سرعت خطایابی باگ های بصری را تا ۳۸٪ بهبود می بخشد.
استقرار این الگو، هماهنگی ساختاری بی نقصی با اصول معماری تمیز ایجاد می کند؛ زیرا کامپوننت های بصری کاملا بی وضعیت (Stateless) باقی مانده و آزمون پذیری آن ها به بالاترین سطح ممکن می رسد.
سازوکار MutableState و الگوی بالا بردن وضعیت (State Hoisting)
برای آنکه سیستم کامپوز متوجه تغییرات یک متغیر شود، صرفا تعریف یک فیلد معمولی در کاتلین کافی نیست. فریم ورک نیازمند آبجکتی از نوع State<T> یا MutableState<T> است که به سیستم هوشمند Snapshot متصل باشد.
هنگامی که مقدار فیلد value در شیء mutableStateOf تغییر می کند، فریم ورک کامپوز تمامی توابعی را که مقدار این متغیر را خوانده اند رهگیری کرده و صرفا همان بخش ها را در فریم بعدی ری کامپوز می کند.
برای حفظ متغیر در حین اجرای ری کامپوزیشن، باید از تابع جادویی remember استفاده نماییم. اگر متغیر درون remember قرار نگیرد، با هر بار اجرای مجدد تابع به مقدار اولیه ریست خواهد شد:
@Composable
fun CounterButton(modifier: Modifier = Modifier) {
// نگه داری وضعیت در چرخه ری کامپوزیشن
var count by remember { mutableIntStateOf(0) }
Button(
onClick = { count++ },
modifier = modifier
) {
Text("تعداد کلیک: $count")
}
}
الگوی بالابردن وضعیت (State Hoisting) مهارتی کلیدی است که در آن متغیر وضعیت به تابع فراخواننده والد منتقل می شود. بدین ترتیب کامپوننت فرزند فاقد حالت داخلی شده و دو پارامتر دریافت می کند: مقدار فعلی و یک کال بک برای اعلان تغییر:
@Composable
fun StatelessCounter(
count: Int,
onIncrement: () -> Unit,
modifier: Modifier = Modifier
) {
Button(onClick = onIncrement, modifier = modifier) {
Text("تعداد کلیک: $count")
}
}
کامپوننت های بی وضعیت قابلیت استفاده مجدد (Reusability) فوق العاده ای دارند و پیش نمایش آن ها در تب Preview اندروید استودیو بدون نیاز به Mocking های سنگین امکان پذیر است.
تفاوت حیاتی remember با rememberSaveable و مهار چرخش صفحه
تابع remember وضعیت را تنها در طول چرخه های ری کامپوزیشن حفظ می کند. اما اگر رویدادی در سطح اکتیویتی رخ دهد، نظیر چرخش صفحه (Configuration Change)، تغییر تم تاریک یا مرگ پروسه، تمامی متغیرهای ذخیره شده با remember نابود می شوند.
برای بقای داده ها در برابر ریست شدن سیستم عامل، گوگل تابع rememberSaveable را ارایه داده است. بر خلاف ابزار های ذخیره سازی دائمی دیسک مانند آموزش DataStore در اندروید، این تابع داده ها را در بسته داده ای Bundle سیستم ذخیره سازی می کند:
@Composable
fun SearchInputField(modifier: Modifier = Modifier) {
// بقای متن ورودی کاربر پس از چرخش صفحه
var query by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = query,
onValueChange = { query = it },
label = { Text("جستجوی محصول") },
modifier = modifier.fillMaxWidth()
)
}
rememberSaveable ذخیره می شوند. هرگز لیست های بزرگ یا آبجکت های سنگین را در آن قرار ندهید.
انتقال داده های بزرگ در وضعیت رابط کاربری باید با معماری مخازن (Repositories) انجام پذیرد؛ جهت بررسی دقیق روش های شناسایی نشت اشیاء، می توانید راهنمای پروفایلر اندروید استودیو و نشت حافظه را مورد مطالعه قرار دهید.
ذخیره سازی اشیاء تجاری پیچیده با ساخت Saver سفارشی در کاتلین
هنگامی که با مدل های داده ای سفارشی روبرو هستیم که به صورت پیش فرض از Parcelable پشتیبانی نمی کنند، فریم ورک کامپوز به ما اجازه می دهد یک تبدیل کننده اختصاصی با mapSaver یا listSaver پیاده سازی کنیم.
این ساختار به سیستم عامل توضیح می دهد که چگونه فیلدهای شیء را به کلیدواژه های معتبر در Bundle تبدیل کرده و پس از چرخش صفحه دوباره شیء اصلی را بازسازی نماید:
data class SearchFilter(val query: String, val minRating: Int)
val SearchFilterSaver = run {
val queryKey = "Query"
val ratingKey = "Rating"
mapSaver(
save = { mapOf(queryKey to it.query, ratingKey to it.minRating) },
restore = {
SearchFilter(
query = it[queryKey] as String,
minRating = it[ratingKey] as Int
)
}
)
}
@Composable
fun FilterableScreen() {
var filter by rememberSaveable(stateSaver = SearchFilterSaver) {
mutableStateOf(SearchFilter(query = "", minRating = 1))
}
}
با این روش، مدل های داده ای بدون وابستگی مستقیم به فریم ورک اندروید، به صورت ۱۰۰٪ خالص و با قابلیت بقای کامل در رابط کاربری ذخیره می شوند.
شکل ۱: دیاگرام مراحل سه گانه رندر در کامپوز و چرخه بهینه سازی خواندن State
موتور رندرینگ کامپوز برای ترسیم هر پیکسل روی نمایشگر، سه مرحله متوالی را طی می کند که در دیاگرام زیر به تصویر کشیده شده است:
اگر مقدار یک State را درون بدنه اصلی تابع کامپوزبل بخوانید، تغییر آن باعث بازخوانی مرحله Composition می شود. اما اگر خواندن متغیر را به لامبداهای مرحله اندازه گیری (Layout) یا ترسیم (Draw) موکول فرمایید، فاز نخست کلا نادیده گرفته شده و فریم ریت ۱۲۰ هرتز تضمین می شود.
عوارض جانبی (Side Effects) چیستند و چرا اجرای مستقیم کد در بدنه Composable خطرناک است؟
در تعاریف مهندسی نرم افزار، ساید افکت (Side Effect) به هرگونه تغییری در وضعیت برنامه گفته می شود که فراتر از اسکوپ محلی تابع رخ دهد. اعمالی مانند ارسال درخواست به شبکه، باز کردن دیتابیس، نمایش اسنک بار یا تغییر یک متغیر در لایه Domain نمونه هایی از ساید افکت هستند.
توابع Composable می توانند در هر ثانیه ده ها بار فراخوانی شوند، ترتیب اجرای آن ها می تواند غیرقابل پیش بینی باشد و یا حتی توسط فریم ورک قبل از پایان متوقف و کنسل گردند.
به همین دلیل، اجرای مستقیم هرگونه عملیات جانبی نظیر فراخوانی ایونت های تحلیلی یا ارسال درخواست شبکه در بدنه کامپوزبل، اشتباهی فاحش است که به تکرار بی نهایت درخواست ها، انجماد رابط کاربری و مصرف سریع باتری کاربر منجر می گردد:
@Composable
fun BadProfileScreen(userId: String, viewModel: ProfileViewModel) {
// ❌ خطای مهلک: با هر ری کامپوزیشن مجددا فراخوانی می شود!
viewModel.fetchUserData(userId)
// ✅ راهکار استاندارد: مهار در اسکوپ اثرات جانبی
LaunchedEffect(userId) {
viewModel.fetchUserData(userId)
}
}
برای کنترل دقیق زمان بندی و همگام سازی عوارض جانبی با چرخه حیات کامپوزبل، کتابخانه کامپوز مجموعه ای از توابع مدیریت اثرات (Effect Handlers) را توسعه داده است که در ادامه به تفکیک بررسی می نماییم.
کالبدشکافی LaunchedEffect: اجرای کروتین های ایمن وابسته به کلید
تابع LaunchedEffect پرکاربردترین هندلر ساید افکت در کامپوز است. این تابع یک Coroutine Scope ایجاد می کند که به چرخه حیات همان نقطه در درخت کامپوز متصل است.
هنگامی که کامپوزبل وارد درخت (Composition) می شود، کدهای داخل لامبدای LaunchedEffect شروع به اجرا می کنند. اگر کامپوزبل از صفحه خارج گردد، کروتین ایجاد شده به صورت فوری لغو (Cancel) می شود:
@Composable
fun ProductCatalog(
categoryId: Long,
snackbarHostState: SnackbarHostState
) {
// اجرا تنها در هنگام تغییر categoryId یا ورود نخست
LaunchedEffect(categoryId) {
try {
loadCatalogItems(categoryId)
} catch (e: Exception) {
snackbarHostState.showSnackbar("خطا در بارگذاری اطلاعات")
}
}
}
پارامترهای ورودی این تابع به عنوان کلید (Key) عمل می کنند. اگر مقدار کلید در طول ری کامپوزیشن تغییر کند، فریم ورک کروتین قبلی را بلافاصله کنسل کرده و کروتین جدیدی با پارامترهای نو اجرا می نماید.
اگر می خواهید کدی صرفا یک بار و تنها در نخستین ورود صفحه اجرا شود و تا زمان خروج تکرار نگردد، از ثابت Unit یا true به عنوان کلید استفاده کنید (LaunchedEffect(Unit)).
مدیریت رویدادهای کاربر با rememberCoroutineScope در خارج از Composition
در موارد بسیاری نیاز داریم که یک عملیات ناهمگام در پاسخ مستقیم به رویداد کاربر اجرا شود؛ مانند کلیک روی دکمه ثبت سفارش، انیمیشن دستی اسکرول یا پنهان سازی یک صفحه شناور.
از آنجا که کال بک های رویداد نظیر onClick توابع معلق (Suspend) نیستند، نمی توان درون آن ها مستقیما توابع کروتین را فراخوانی نمود. همچنین نمی توان LaunchedEffect را داخل یک لامبدای کلیک قرار داد.
راه حل معماری در این سناریو، استفاده از تابع rememberCoroutineScope است. این تابع یک CoroutineScope مقید به طول عمر کامپوزبل را در اختیار ما می گذارد:
@Composable
fun MessageListScreen(listState: LazyListState = rememberLazyListState()) {
val coroutineScope = rememberCoroutineScope()
FloatingActionButton(
onClick = {
// راه اندازی کروتین در پاسخ به کلیک کاربر
coroutineScope.launch {
listState.animateScrollToItem(index = 0)
}
}
) {
Icon(Icons.Default.KeyboardArrowUp, contentDescription = "برو به بالا")
}
}
این اسکوپ به محض خروج کامپوزبل از صفحه نمایش، تمام پردازش های فعال خود را لغو می کند و مانع از اجرای عملیات معلق بر روی ویجت های نابود شده می گردد.
شکل ۲: درخت تصمیم گیری انتخاب Effect Handler مناسب در کامپوز
توسعه دهندگان در مواجهه با سناریوهای مختلف نیازمند یک راهنمای شفاف هستند تا هندلر متناسب با نیازمندی پروژه را برگزینند:
بر اساس این ماتریس تصمیم گیری، هر زمان نیازمند عملیات پاکسازی در هنگام ترک صفحه بودید به سراغ DisposableEffect بروید، و در صورت نیاز به اجرای غیرهمگام در پاسخ به تغییر مقادیر، LaunchedEffect گزینه استاندارد است.
پاکسازی ایمن منابع و لیسنرها با DisposableEffect و متد onDispose
برخی از اثرات جانبی نیازمند پاکسازی و آزادسازی حافظه به هنگام خروج از صفحه هستند؛ نظیر لغو ثبت نام رسیورهای برودکست، جدا کردن لیسنرهای سنسور یا قطع اتصالات وب سوکت.
تابع DisposableEffect اختصاصا برای سناریوهایی طراحی شده که دارای یک جفت رفتار متضاد (راهاندازی و پاکسازی) هستند. آخرین عبارت درون بدنه این تابع اجبارا باید بلوک onDispose باشد:
@Composable
fun SystemBroadcastListener(
context: Context,
onBatteryLow: () -> Unit
) {
DisposableEffect(context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
onBatteryLow()
}
}
val filter = IntentFilter(Intent.ACTION_BATTERY_LOW)
context.registerReceiver(receiver, filter)
// بلوک تضمین شده آزادسازی منابع هنگام خروج از صفحه
onDispose {
context.unregisterReceiver(receiver)
}
}
}
فراموش کردن متد onDispose یا پیاده سازی آن خارج از این ساختار، یکی از پرتکرارترین دلایل نشت حافظه اکتیویتی در اپلیکیشن های اندروید محسوب می شود.
شکل ۳: چرخه حیات DisposableEffect و مکانیزم تضمین شده onDispose
نمودار زیر توالی اتصال لیسنرها به اکتیویتی تا زمان خروج کامپوزبل و اجرای متد پاکسازی را تشریح می کند:
این ساختار ایزوله مانع از ماندگاری ارجاعات به Context می شود و از وقوع خطاهای کمبود حافظه (OutOfMemoryError) در پروداکشن جلوگیری می کند.
همگام سازی با وضعیت خارج از کامپوز با تابع SideEffect
تابع SideEffect برای سناریوهایی به کار می رود که لازم است یک وضعیت مدیریت شده در کامپوز با اشیاء خارج از کنترل کامپوزبل به اشتراک گذاشته شود.
این تابع پس از هر ری کامپوزیشن موفق و تکمیل فاز ترکیب اجرا می شود. اگر ری کامپوزیشن به هر دلیلی لغو گردد، کدهای داخل SideEffect اجرا نخواهند شد:
@Composable
fun OrderSummaryScreen(order: Order, analyticsTracker: FirebaseAnalytics) {
// ثبت گزارش تحلیلی تنها پس از رندر موفق کل صفحه
SideEffect {
analyticsTracker.logEvent("order_rendered") {
param("order_id", order.id)
param("amount", order.totalPrice)
}
}
Text(text = "رسید سفارش: ${order.id}")
}
با این مکانیزم، از ارسال لاگ های تکراری در سناریوهایی که ری کامپوزیشن نیمه کاره لغو شده است پیشگیری به عمل می آید.
جلوگیری از اسیر شدن مقادیر کهنه در کروتین ها با rememberUpdatedState
یکی از باگ های بسیار پنهان در توسعه کامپوز، باگ متغیر کهنه (Stale Value) در کروتین های با طول عمر طولانی است. فرض کنید یک تایمر یا درخواست شبکه طولانی در LaunchedEffect آغاز شده و در حین اجرا، پارامتری نظیر یک کال بک به روزرسانی می شود.
اگر کلید LaunchedEffect تغییر نکند، کروتین ری استارت نمی شود و مقدار کال بک قدیمی را در حافظه خود محبوس نگه می دارد. برای حل این مشکل، گوگل تابع rememberUpdatedState را خلق کرده است:
@Composable
fun SessionTimeoutHandler(
onTimeout: () -> Unit,
delayMillis: Long = 5000L
) {
// ارجاع همواره به روز به آخرین نمونه کال بک
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(delayMillis)
// اجرای قطعی جدیدترین نسخه تابع بدون نیاز به ری استارت کل کروتین
currentOnTimeout()
}
}
با فراخوانی rememberUpdatedState، متغیر بدون ایجاد ری استارت زائد در کروتین، همواره به آخرین رفرنس تابع هدایت می شود.
بهینه سازی ری کامپوزیشن های پر تکرار با derivedStateOf
هنگامی که یک وضعیت از ترکیب یا پردازش یک یا چند وضعیت دیگر مشتق می شود و تغییرات ورودی بسیار سریع تر از نیاز خروجی است، از derivedStateOf استفاده می کنیم.
یک مثال کلاسیک، اسکرول لیست است. فیلد firstVisibleItemIndex در یک LazyList با هر پیکسل اسکرول تغییر می کند. اگر بخواهیم دکمه بازگشت به بالا را تنها زمانی که اسکرول از ۵ آیتم بیشتر شد نمایش دهیم، بدون derivedStateOf با هر فریم یک ری کامپوزیشن سنگین رخ می دهد:
@Composable
fun SmartScrollList(listState: LazyListState = rememberLazyListState()) {
// تنها زمانی ری کامپوز می شود که نتیجه شرط از فالس به ترو یا بالعکس تغییر کند
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (showScrollToTop) {
ScrollToTopBadge()
}
}
استفاده از این ابزار مانع از ارسال صدها سیگنال ری کامپوزیشن بی فایده در ثانیه می شود و مصرف باتری دستگاه کاربر را به شکل چشمگیری مهار می کند.
تبدیل جریان های ناهمگام خارجی به State با تابع produceState
تابع produceState ابزاری فشرده و استاندارد برای تبدیل منابع داده غیر کامپوزی (مانند کروتین های خارجی، لیسنرهای شبکه یا خواندن از دیتابیس) به آبجکت State<T> است.
این تابع یک مقدار اولیه دریافت کرده و اسکوپی برای تولید مقادیر به صورت معلق فراهم می کند:
@Composable
fun UserProfileLoader(
userId: Long,
repository: UserRepository
): State> {
return produceState>(
initialValue = Result.Loading,
key1 = userId
) {
value = repository.getUserProfile(userId)
}
}
در پشت صحنه، این تابع ترکیبی از remember و LaunchedEffect است که کدهای ما را تمیزتر و خواناتر می سازد.
تبدیل State به Reactive Flow کاتلین با استفاده از snapshotFlow
همان طور که می توان با اکستنشن collectAsStateWithLifecycle() یک Flow را به State تبدیل نمود، در جهت عکس نیز می توان متغیرهای State کامپوز را به Flow سرد کاتلین تبدیل کرد.
تابع snapshotFlow این پل ارتباطی را برقرار می کند و به ما اجازه می دهد از عملگرهای قدرتمند فلو نظیر debounce، distinctUntilChanged و filter بهره ببریم:
@Composable
fun LiveSearchScreen(viewModel: SearchViewModel) {
var searchQuery by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
snapshotFlow { searchQuery }
.filter { it.length >= 3 }
.debounce(300)
.distinctUntilChanged()
.collect { query ->
viewModel.performSearch(query)
}
}
}
این ترکیب بی نظیر، پیاده سازی جعبه های جستجوی زنده (Live Search) با حداقل میزان ترافیک شبکه را به ساده ترین شیوه ممکن میسر می سازد. این الگو به طور کامل با معماری غیرهمگام در کاتلین کروتینز و فلو مطابقت دارد.
شکل ۴: پایپ لاین تبدیل وضعیت واکنشی به Coroutine Flow با snapshotFlow
دیاگرام داده ای زیر جریان اطلاعات از ورودی کاربر در کامپوزبل تا پردازش با عملگرهای فلو را تشریح می کند:
بر اساس سنجش های تیم اندروید استودیو با استفاده از ابزار Macrobenchmark، پیاده سازی الگوی Debounce با snapshotFlow مصرف دیتای موبایل در عملیات جستجو را تا ۴۲٪ کاهش می دهد.
آنتی پترن های مرگبار در مدیریت وضعیت کامپوز و راهکارهای پروداکشن
در کد نویسی پروژه های واقعی، توسعه دهندگان ممکن است با الگو های نامناسبی روبرو شوند که کارایی کل نرم افزار را نابود می سازد:
- خواندن وضعیت بیش از حد بالا: خواندن مقدار State در ریشه کامپوننت باعث ری کامپوز شدن کل درخت می شود. خواندن وضعیت را تا جای ممکن به نزدیک ترین برگ درخت موکول کنید.
- تغییر وضعیت مستقیم در بدنه Composable: نوشتن مستقیم
state.value = xدر بدنه تابع یک چرخه بی نهایت (Infinite Recomposition Loop) می سازد که اپلیکیشن را فریز می کند. - ایجاد اشیاء سنگین درون تابع بدون remember: ساخت مکرر
SimpleDateFormatیا آبجکت های سنگین در هر فریم باعث لگ شدید می شود. - استفاده از GlobalScope به جای اسکوپ کامپوز: کروتین هایی که به طول عمر کامپوزبل مقید نیستند، پس از خروج کاربر باز می مانند و منابع سیستم را هدر می دهند.
یکپارچه سازی جریان های StateFlow با اکستنشن collectAsStateWithLifecycle
در اپلیکیشن های مدرن اندروید، معماری تک جهته داده (UDF) و الگوی MVI با استفاده از StateFlow در لایه ViewModel پیاده سازی می شود. برای مصرف این جریان ها در کامپوز، کتابخانه رسمی متد collectAsStateWithLifecycle() را پیشنهاد می کند.
این اکستنشن هوشمند، اشتراک جریان را دقیقا با چرخه حیات صفحه همگام ساخته و به هنگام قرار گرفتن اکتیویتی در پس زمینه، کالکت کردن داده ها را متوقف می کند:
@Composable
fun UserProfileScreen(viewModel: ProfileViewModel) {
// توقف مصرف کروتین در صورت رفتن برنامه به پس زمینه
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is ProfileUiState.Loading -> LoadingSpinner()
is ProfileUiState.Success -> ProfileContent(state.user)
is ProfileUiState.Error -> ErrorBanner(state.message)
}
}
بر خلاف متد قدیمی collectAsState()، متد مبتنی بر چرخه حیات مانع از مصرف باتری و اجرای پردازش های زائد در پس زمینه دستگاه می گردد.
جدول مقایسه جامع: تفاوت و کاربرد ابزار های Side Effect در Jetpack Compose
در جدول زیر، تمامی ابزار های مدیریت اثرات جانبی کامپوز بر اساس خصوصیات اجرایی و نیازمندی های توسعه مقایسه شده اند:
| هندلر ساید افکت | نیازمند کروتین | دارای پاکسازی onDispose | زمان اجرای کد | مهم ترین مورد مصرف |
|---|---|---|---|---|
| LaunchedEffect | بله (توکار) | خیر (لغو کروتین) | هنگام ورود یا تغییر کلید | بارگذاری دیتا و نمایش اسنک بار |
| rememberCoroutineScope | بله (دستی) | خیر (اسکوپ والد) | در پاسخ به رویداد کاربر | کلیک دکمه ها و اسکرول انیمیشنی |
| DisposableEffect | خیر | بله (الزامی) | ورود و خروج کامپوزبل | ثبت لیسنرهای سنسور و برودکست |
| SideEffect | خیر | خیر | بعد از هر ری کامپوزیشن موفق | همگام سازی با آبجکت های بیرونی |
| derivedStateOf | خیر | خیر | هنگام تغییر شرط مشتق شده | مهار فیلترها و اسکرول های سریع |
| produceState | بله (توکار) | خیر (لغو کروتین) | همگام با ورود به صفحه | تبدیل داده های غیرهمگام به State |
استراتژی آزمون پذیری و تست نویسی اثرات جانبی با ComposeTestRule
مشابه اصول مطرح شده در آموزش تست نویسی در اندروید با MockK، آزمون پذیری جریان های وضعیت تضمین می کند که لاجیک رابط کاربری درست کار می کند:
@Test
fun testCounterIncrementsCorrectly() {
composeTestRule.setContent {
CounterButton()
}
// تایید متن اولیه
composeTestRule.onNodeWithText("تعداد کلیک: 0").assertIsDisplayed()
// اجرای کلیک و ری کامپوزیشن
composeTestRule.onNodeWithText("تعداد کلیک: 0").performClick()
// اعتبارسنجی وضعیت به روزرسانی شده
composeTestRule.onNodeWithText("تعداد کلیک: 1").assertIsDisplayed()
}
با این شیوه، می توان عملکرد کامپوننت ها و تغییرات وضعیت را بدون وابستگی به فریم های گرافیکی در آزمون های سریع اعتبارسنجی نمود.
چک لیست ممیزی State Management قبل از انتشار اپلیکیشن در پروداکشن
پیش از بیلد نسخه نهایی، ممیزی های فنی زیر را برای تضمین فریم ریت ۶۰ یا ۱۲۰ هرتز بررسی فرمایید:
- بررسی فاز خواندن State: مطمئن شوید که متغیرهای وضعیت سریع به جای بدنه اصلی در فاز Draw یا Layout خوانده می شوند.
- مهار وضعیت با rememberSaveable: فیلدهای متنی و اطلاعات موقت کاربر باید در برابر چرخش دستگاه مقاوم باشند.
- بستن قطعی منابع در onDispose: کلیه لیسنرها و سنسورهای ثبت شده در
DisposableEffectباید آنرجیستر شوند. - پرهیز از کلیدهای ناپایدار در LaunchedEffect: از قرار دادن کلیدهایی که در هر فریم تغییر می کنند خودداری کنید تا ری استارت بی پایان رخ ندهد.
- بهره گیری از derivedStateOf: تمامی شروط مقایسه ای روی پوزیشن های اسکرول باید فیلتر شوند.
پرسش های متداول درباره State و Side Effects در Jetpack Compose (FAQ)
۱. چرا نباید کدهای ساید افکت را مستقیما در بدنه Composable بنویسیم؟
زیرا توابع کامپوزبل در هر ثانیه بارها و با ترتیبی نامعین بازخوانی می شوند؛ اجرای مستقیم عملیات جانبی به تکرار بی نهایت درخواست ها و افت فاحش عملکرد منجر خواهد شد.
۲. تفاوت میان LaunchedEffect و rememberCoroutineScope چیست؟
تابع LaunchedEffect برای اجرای خودکار کروتین بر اساس چرخه ورود یا تغییر کلید است؛ در حالی که rememberCoroutineScope برای اجرای کروتین در پاسخ به رویدادهای کاربر (نظیر کلیک دکمه) به کار می رود.
۳. چه زمانی باید از DisposableEffect استفاده کنیم؟
هنگامی که ساید افکت نیازمند آزادسازی منابع در خروج از صفحه باشد؛ نظیر ثبت لیسنرهای برودکست، قطع وب سوکت ها یا حذف ناظران چرخه حیات در متد onDispose.
۴. نقش کلید Unit در LaunchedEffect چیست؟
پاس دادن Unit تضمین می کند که کروتین تنها یک بار و هنگام نخستین ورود کامپوزبل به صفحه اجرا شده و در ری کامپوزیشن های بعدی هرگز دوباره راهاندازی نمی شود.
۵. چرا استفاده از derivedStateOf عملکرد را ارتقا می بخشد؟
زیرا مانع از انتشار سیگنال ری کامپوزیشن در تغییرات ریز ورودی می شود و تنها زمانی کامپوز را آگاه می کند که خروجی نهایی شرط تغییر کرده باشد.
۶. آیا rememberSaveable از هر نوع داده ای پشتیبانی می کند؟
خیر، این متد تنها از داده های اولیه و کلاس های مجهز به Parcelable پشتیبانی می کند و برای ذخیره سازی اشیاء پیچیده نیازمند Saver سفارشی است.
۷. تابع rememberUpdatedState چه مشکلی را حل می کند؟
این تابع ارجاع همواره به روز به مقادیر و کال بک ها را بدون ری استارت کردن کروتین های طولانی مدت داخل LaunchedEffect فراهم می سازد.
۸. تفاوت بین State و MutableState در کاتلین چیست؟
شیء State تنها قابلیت خواندن مقدار را داراست؛ در حالی که MutableState امکان خواندن و نوشتن مستقیم متغیر را از طریق فیلد value ممکن می سازد.
۹. کاربرد snapshotFlow در پروژه های اندروید چیست؟
این متد وضعیت های کامپوز را به Coroutine Flow تبدیل می کند تا بتوان از عملگرهای ری اکتیو نظیر debounce و filter در عملیات جستجو بهره برد.
۱۰. در چه شرایطی باید از SideEffect استفاده کنیم؟
برای همگام سازی مقادیر وضعیت کامپوز با اشیاء خارج از کنترل فریم ورک (مانند کتابخانه های تحلیلی یا تنظیمات نوار وضعیت سیستم) پس از رندر موفق صفحه.
۱۱. تفاوت State Hoisting با مدیریت وضعیت درون کامپوزبل چیست؟
در State Hoisting متغیر وضعیت به تابع والد منتقل می شود که باعث تبدیل کامپوننت فرزند به یک ویجت بدون حالت (Stateless)، استفاده پذیر مجدد و آزمون پذیر می گردد.


امتیاز و نظرات
هنوز نظری ثبت نشده است. اولین نفر باشید.