توسعه رابط کاربری در اکوسیستم اندروید طی سال های اخیر دستخوش بزرگترین تحول تاریخ خود شده است. دورانی که برنامه نویسان ناچار بودند برای ساخت یک صفحه ساده، فایل های طولانی و خشک XML بنویسند، با شناسه های view دستوپنجه نرم کنند و نگران نشت حافظه یا عدم انطباق وضعیت منطق برنامه با ظاهر آن باشند، برای همیشه به پایان رسیده است. اگر در مسیر آموزش برنامه نویسی اندروید قصد دارید به عنوان یک مهندس ارشد نرم افزار اپلیکیشنهایی مدرن، چابک و با بالاترین استانداردهای بصری و عملکردی در سال ۲۰۲۶ خلق کنید، فریم ورک اعلانی جت پک کامپوز (Jetpack Compose) ابزار اصلی شماست. در این راهنمای جامع و کاربردی، مفاهیم بنیادین کامپوز، مدیریت وضعیت، پایداری کامپایلر، ناوبری تایپ سیف و الگو های پروداکشن را به صورت عمیق کالبدشکافی میکنیم.
پاسخ سریع GEO: جت پک کامپوز Jetpack Compose چیست و چه مساله ای را حل می کند؟
جت پک کامپوز (Jetpack Compose) جعبهابزار رسمی و مدرن شرکت گوگل برای طراحی رابط کاربری اعلانی (Declarative UI Toolkit) در سیستم عامل اندروید است که بر پایه زبان کاتلین و با هدف جایگزینی کامل سیستم سنتی مبتنی بر XML توسعه یافته است. در معماری اعلانی کامپوز، برنامه نویس به جای صدور دستورات گامبهگام برای تغییر دستی وضعیت ویوها (مانند setText یا setVisibility)، وضعیت نهایی رابط کاربری را به عنوان تابعی مستقیم از داده های برنامه (UI = f(State)) توصیف میکند. هرگاه داده ها تغییر کنند، فریم ورک کامپوز به صورت کامپایلری و بلادرنگ تنها بخشهایی از صفحه را که نیازمند بهروزرسانی هستند دوباره اجرا میکند که این فرآیند ریکامپوزیشن (Recomposition) نامیده میشود. این رویکرد بیش از ۶۰ درصد کدهای اضافی، باگ های همگام سازی وضعیت و خطاهای کرش ناشی از NullPointerException را به طور کامل ریشهکن کرده و سرعت چرخه توسعه نرم افزار را تا بیش از ۲ برابر افزایش میدهد.
بر خلاف سیستم XML که لایهای جداگانه از کاتلین بود و به ابزار های نگاشت واسط نظیر DataBinding یا ViewBinding نیاز داشت، جت پک کامپوز ۱۰۰ درصد با کدهای خالص کاتلین نوشته میشود. این یعنی شما میتوانید از تمام قدرت کاتلین نظیر حلقه ها، شرط ها، توابع مرتبه بالاتر، اکستنشن ها و کورتین ها مستقیماً درون تعریف لایه های رابط کاربری بهره ببرید.
پارادایم شیفت بزرگ: گذر از UI دستوری به سیستم اعلانی (Declarative vs Imperative)
برای تسلط واقعی بر جت پک کامپوز، پیش از هر چیز باید مدل ذهنی خود را از پارادایم دستوری (Imperative) به پارادایم اعلانی (Declarative) ارتقا دهید. در سیستم های سنتی UI اندروید که بیش از یک دهه حاکم بودند، سلسلهمراتب ویوها به شکل یک درخت بزرگ از اشیاء سنگین کلاس View در حافظه ساخته میشد. در آن رویکرد، وظیفه بهروزرسانی رابط کاربری به عهده توسعه دهنده بود:
- پیمایش دستی درخت ویوها (findViewById): پیدا کردن ویجت مورد نظر در سلسلهمراتب حافظه که فرآیندی پرهزینه و خطازا بود.
- مدیریت چندمنبعی وضعیت (State Inconsistency): اگر یک متغیر تغییر میکرد اما برنامه نویس فراموش میکرد متد
setTextیاsetImageResourceرا فراخوانی کند، ظاهر برنامه با داده های درونی دچار مغایرت میشد. - پیچیدگی تم بندی و انیمیشن ها: ساخت انیمیشن های چندمرحلهای در XML نیازمند فایل های متعدد و کدهای پیچیده کالبک در فرگمنت ها یا اکتیویتی ها بود.
در نقطه مقابل، جت پک کامپوز رابط کاربری را به صورت توابع تغییرناپذیر (Immutable) تعریف میکند. توابع کامپوزبل با انوتیشن @Composable نشانهگذاری میشوند. این توابع به جای اینکه یک آبجکت ویو برگردانند، به فریم ورک کامپوز اعلام میکنند که صفحه باید چه شکلی باشد. هر بار که داده ورودی تغییر کند، کامپوز تابع را مجدداً با داده جدید اجرا میکند، بدون اینکه نیاز به دستکاری دستی عناصر صفحه باشد. همانطور که در راهنمای مقایسه کاتلین و جاوا در برنامه نویسی اندروید دیدیم، امکانات زبان کاتلین مدرن بستر اصلی این تحول عظیم است.
کالبدشکافی فازهای ۳ گانه رندرینگ اعلانی: Composition, Layout, Drawing
موتور رندرینگ کامپوز برای تبدیل توابع کاتلین به پیکسل های روی نمایشگر گوشی، سه مرحله مشخص را با بهینه سازی خارقالعاده طی میکند. درک این سه فاز تفاوت یک توسعه دهنده معمولی با یک مهندس ارشد کارایی است:
- فاز ترکیب بندی (Composition Phase): در این فاز، کامپایلر کامپوز توابع
@Composableرا اجرا کرده و درخت معنایی رابط کاربری (UI Composition Tree) را تشکیل میدهد. در این مرحله مشخص میشود چه المانهایی باید در صفحه حضور داشته باشند. - فاز چینش و اندازهگیری (Layout Phase): هر کامپوننت موجود در درخت، فرزندان خود را اندازهگیری کرده و ابعاد دقیق (عرض و ارتفاع) و موقعیت دوبعدی (مختصات X و Y) آن ها را روی صفحه محاسبه میکند. بر خلاف سیستم قدیمی XML که با مشکل اندازهگیری چندباره (Double Measurement Bug) مواجه بود، کامپوز قانونی سفتوسخت دارد: هر نود درخت در هر فریم تنها یک بار حق اندازهگیری دارد (Single-Pass Measurement).
- فاز ترسیم (Drawing Phase): در نهایت، فریم ورک دستورات گرافیکی را به بوم سخت افزاری Skia ارسال کرده و پیکسل های واقعی کارت ها، متن ها، لبه های گرد و تصاویر را روی صفحه نمایش دستگاه کاربر رندر میکند.
اگر متغیری که تغییر میکند تنها روی فاز Drawing تاثیر دارد (مثلاً تغییر رنگ پسزمینه یا شفافیت Alpha)، استفاده از لامبدا در مودیفایرها (مانند Modifier.drawBehind یا Modifier.graphicsLayer) باعث میشود فازهای سنگین Composition و Layout دور زده شوند و تغییرات با نرخ ۱۲۰ فریم بر ثانیه بدون افت پرفورمنس اعمال گردند.
کالبدشکافی مودیفایرها (Modifiers): زنجیره تنظیمات ظاهری و رفتاری
در سیستم سنتی XML، هر ویژگی ظاهری (مانند padding، background یا onClick) به عنوان یک اتریبیوت درون تگ XML نوشته میشد که این امر باعث حجیم شدن کلاس های پایه View تا هزاران خط کد میشد. در جت پک کامپوز، تمام رفتارهای فرعی، ابعاد، فاصلهگذاری ها، رویدادهای لمسی و انیمیشن ها از طریق یک شیء تغییرناپذیر به نام Modifier هدایت میشوند.
مهمترین نکته مهندسی درباره مودیفایرها این است که ترتیب زنجیره سازی (Chaining Order) اهمیت حیاتی دارد. مودیفایرها دقیقاً به همان ترتیبی که نوشته میشوند از چپ به راست (یا بالا به پایین) بر روی کامپوننت اعمال میگردند:
// تفاوت کلیدی در ترتیب اعمال مودیفایرها
Box(
modifier = Modifier
.clip(RoundedCornerShape(16.dp)) // ابتدا لبه ها گرد می شوند
.background(Color.Blue) // رنگ پس زمینه با لبه های گرد اعمال می شود
.clickable {{ /* رویداد کلیک */ }} // افکت ریپل کلیک نیز گرد خواهد بود
.padding(16.dp) // فاصله داخلی اعمال می شود
) {{
Text("کارت با طراحی مهندسی", color = Color.White)
}}
اگر در کد فوق جای padding و background عوض شود، رنگ آبی فقط محتوای داخلی را میپوشاند و حاشیه بیرونی سفید باقی میماند! همین سادگی در ترکیب پذیری (Composability)، به توسعه دهندگان قدرت میدهد تا هر ویجت اختصاصی پیچیدهای را بدون ساخت کلاس های جدید خلق کنند.
مدیریت وضعیت (State Management) و متغیرهای حیاتی در کامپوز
وضعیت (State) در جت پک کامپوز، هر مقداری است که با گذر زمان میتواند تغییر کند؛ از محتوای یک کادر متنی گرفته تا وضعیت لودینگ شبکه یا اسکرول فهرست. از آنجا که توابع کامپوزبل در هر Recomposition مجدداً از نو اجرا میشوند، اگر یک متغیر ساده درون تابع تعریف شود، با هر بازطراحی به مقدار اولیه خود بازمیگردد! برای حفظ وضعیت در میان چرخه های ریکامپوزیشن، از سازوکارهای اختصاصی استفاده میشود:
۱. عملگر remember: حفظ وضعیت در میان Recomposition ها
تابع remember یک مقدار محاسباتی یا شیء را در حافظه داخلی Composition ذخیره میکند تا در اجرای بعدی تابع دوباره مقداردهی اولیه نشود:
// حفظ وضعیت یک متغیر با remember و mutableStateOf
@Composable
fun CounterButton() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text(text = "تعداد کلیک ها: $count")
}
}
۲. تفاوت remember با rememberSaveable: نجات داده ها در چرخش صفحه
اگرچه remember وضعیت را در برابر ریکامپوزیشن حفظ میکند، اما به محض رخ دادن یک تغییر پیکربندی سیستمی (Configuration Change) نظیر چرخاندن گوشی از حالت عمودی به افقی، تغییر تم سیستم یا تغییر زبان، کل اکتیویتی اندروید ریست شده و متغیرهای remember نیز نابود میشوند!
برای حل این معضل، گوگل تابع rememberSaveable را معرفی کرده است. این تابع داده ها را در پشت صحنه درون Bundle سیستم عامل ذخیره میکند و حتی با بسته شدن اکتیویتی توسط سیستم عامل برای آزادسازی رم (Process Death)، داده ها دستنخورده باقی میمانند.
معماری جریان یک طرفه داده ها (Unidirectional Data Flow - UDF) و اتصال به ویومدل
در پروژه هایی پروداکشن سازمانی، مدیریت مستقیم وضعیت درون خود کامپوننت های UI کاری به شدت خطاست و باعث ایجاد کدهای اسپاگتی میشود. استاندارد طلایی گوگل، جداسازی مسئولیت ها بر پایه الگوی جریان یکطرفه داده ها (UDF) است:
- State جریانی رو به پایین دارد: وضعیت داده ها از ViewModel به سمت کامپوزبل ها سرازیر میشود.
- Events جریانی رو به بالا دارند: رویدادهای کاربر (کلیک دکمه، تایپ متن، سوایپ) به صورت کالبک از کامپوزبل به سمت ViewModel ارسال میگردند.
مطابق با اصول تشریحشده در آموزش معماری تمیز در اندروید، لایه Presentation نباید منطق کسبوکار را بداند؛ بلکه صرفاً وضعیت را مشاهده میکند. در سال ۲۰۲۶، جمعآوری امن داده های StateFlow درون کامپوز با اکستنشن استاندارد collectAsStateWithLifecycle انجام میشود تا با توقف اکتیویتی، مصرف باتری و رم متوقف گردد:
// ساختار استاندارد UDF در اتصال ViewModel به رابط کاربری کامپوز
@Composable
fun ProductCatalogScreen(
viewModel: ProductCatalogViewModel,
onNavigateToDetail: (Long) -> Unit
) {
// دریافت وضعیت با در نظر گرفتن چرخه حیات لایف سایکل
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ProductCatalogContent(
state = uiState,
onRefreshClicked = { viewModel.onEvent(CatalogUiEvent.Refresh) },
onProductClicked = onNavigateToDetail
)
}
سیستم پایداری کامپایلر کامپوز (Compose Stability) و تکنیک پرش هوشمند
یکی از شگفتانگیزترین قابلیت های بهینه سازی جت پک کامپوز، توانایی پرش هوشمند (Smart Skipping) است. وقتی یک Recomposition رخ میدهد، اگر کامپایلر کامپوز تشخیص دهد که پارامترهای ورودی یک تابع کامپوزبل نسبت به فراخوانی قبلی هیچ تغییری نکردهاند، اجرای مجدد آن تابع را به طور کامل دور میزند (اسکیپ میکند) تا پردازنده بدون دلیل مصرف نشود.
اما کامپایلر کامپوز چگونه میفهمد یک پارامتر تغییرناپذیر است؟ از طریق سیستم پایداری داده ها (Stability System):
- انواع پایدار (Stable Types): انواعی که تضمین میکنند مقادیر عمومی آن ها تغییر نمیکند یا در صورت تغییر، کامپوز را از تغییرات باخبر میسازند. انواع پایه کاتلین نظیر
Int،String،Floatو متغیرهایState<T>پایدار محسوب میشوند. - انواع تغییرناپذیر (Immutable Types): کلاسهایی که با انوتیشن
@Immutableعلامتگذاری شده و تمام فیلدهای آن هاvalاست. کامپایلر مطمئن است این داده ها هرگز تغییر نخواهند کرد. - تله بزرگ کالکشن های ناپایدار (Unstable Collections): یکی از بزرگترین عوامل افت کارایی در برنامه هایی اندروید، ارسال کالکشن های استاندارد جاوا و کاتلین نظیر
List<Product>به عنوان پارامتر به کامپوزبل است! کامپایلر کامپوز اینترفیسListرا ناپایدار (Unstable) ارزیابی میکند؛ زیرا ممکن است نمونه واقعی آن در پشت صحنه یکArrayListبا قابلیت تغییر (Mutable) باشد. در نتیجه تابع کامپوزبل در هر فریم دوباره اجرا شده و قابلیت Smart Skipping غیرفعال میشود.
در نسخه های مدرن کاتلین و کامپوز، با فعال سازی قابلیت Strong Skipping Mode در گریدل یا استفاده از کالکشن های تغییرناپذیر رسمی (Kotlinx Immutable Collections نظیر PersistentList<T>)، مشکل ناپایداری کالکشن ها به کلی حل شده و روانی اسکرول لیست ها حتی با هزاران آیتم به ۶۰ تا ۱۲۰ فریم پایدار میرسد.
انیمیشن های اعلانی در کامپوز: خلق تجربه کاربری روان با نرخ ۱۲۰ فریم
در سیستم سنتی اندروید، پیاده سازی یک انیمیشن ساده ورود و خروج نیازمند ساخت فایل های متعدد XML در پوشه res/anim، مقداردهی کلاس های سنگین ValueAnimator و مدیریت دستی کالبک های پایان حرکت بود. این فرآیند علاوه بر ایجاد کدهای طولانی، مستعد بروز نشت حافظه (Memory Leak) در صورت متوقف شدن زودهنگام اکتیویتی بود.
در جت پک کامپوز، انیمیشن ها همانند ظاهر برنامه ماهیتی کاملاً اعلانی (Declarative) دارند. شما تنها وضعیت هدف (Target State) را مشخص میکنید و فریم ورک به صورت خودکار محاسبات فریم ها و انتقال مقادیر را با ریاضیات روان فیزیکی (Spring Physics) یا منحنی های Bezier مدیریت مینماید:
۱. ورود و خروج هوشمند عناصر با AnimatedVisibility
کامپوننت قدرتمند AnimatedVisibility به شما امکان میدهد نمایش یا پنهان شدن هر بخش از صفحه را با ترکیبی از ترنزیشن های محوشدگی، مقیاس و جابجایی انیمیت کنید:
// انیمیشن ترکیبی ورود و خروج کارت خطا یا هشدار
@Composable
fun OrderStatusBanner(isVisible: Boolean, message: String) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(animationSpec = tween(300)) + slideInVertically(
initialOffsetY = { -it / 2 }
),
exit = fadeOut(animationSpec = tween(200)) + shrinkVertically()
) {
Card(
modifier = Modifier.fillMaxWidth().padding(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
) {
Text(
text = message,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
۲. تغییر نرم خصوصیات با توابع animate*AsState
هرگاه بخواهید یک رنگ، اندازه (Dp)، زاویه چرخش، یا شفافیت را به آرامی تغییر دهید، کافی است از خانواده توابع animate*AsState استفاده کنید. این توابع با هر تغییر متغیر، یک شیء State انیمیتشده تولید میکنند:
// انیمیشن تغییر ابعاد و گوشه های کارت در زمان انتخاب
@Composable
fun SelectableCard(isSelected: Boolean, onClick: () -> Unit) {
val cornerRadius by animateDpAsState(
targetValue = if (isSelected) 24.dp else 8.dp,
animationSpec = spring(stiffness = Spring.StiffnessLow),
label = "corner_animation"
)
Box(
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(cornerRadius))
.background(if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant)
.clickable(onClick = onClick)
)
}
برای انیمیشنهایی نظیر چرخش، بزرگنمایی (Scale) یا تغییر موقعیت در هنگام اسکرول، هرگز متغیر انیمیشن را مستقیماً به پارامترهای مودیفایر نظیر Modifier.offset با مقدار عددی پاس ندهید! در عوض از نسخه لامبدای Modifier.graphicsLayer { translationY = animatedOffset } استفاده کنید. این تکنیک باعث میشود فاز سنگین Recomposition به طور کامل دور زده شده و انیمیشن منحصراً در لایه پردازنده گرافیکی (GPU) با نرخ ۱۲۰ فریم بر ثانیه اجرا گردد.
پیاده سازی عملیاتی یک رابط کاربری کامل پروداکشن با کاتلین
برای لمس واقعی قدرت کامپوز، بیایید یک صفحه نمایش کاتالوگ دوره های آموزشی با قابلیت های زیر بسازیم:
- استفاده از ساختار استاندارد لایه بندی با
ScaffoldوTopAppBarدر زبان متریال ۳ (Material ۳). - فهرست روان و با کارایی بالا با
LazyColumnمجهز بهkeyوcontentTypeبرای پرهیز از رندرهای تکراری. - کارت دوره با طراحی مدرن، افکت کلیک، لبه های گرد و انیمیشن تغییر وضعیت خرید.
package ir.vahidhoseini.courses.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
// ۱. مدل داده ای پایدار با فیلدهای تغییرناپذیر
@Immutable
data class CourseUiModel(
val id: Long,
val title: String,
val instructor: String,
val priceTomans: Long,
val isEnrolled: Boolean
)
// ۲. کامپوننت ریشه با ساختار Scaffold و Material 3
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CourseCatalogScreen(
courses: List,
onCourseClick: (Long) -> Unit,
onEnrollClick: (Long) -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("کاتالوگ دوره های مهندسی نرم افزار", fontWeight = FontWeight.Bold) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { innerPadding ->
// استفاده از LazyColumn با کلید یکتا برای حداکثر کارایی در اسکرول
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 16.dp)
) {
items(
items = courses,
key = { course -> course.id },
contentType = { "course_card" }
) { course ->
CourseCardItem(
course = course,
onClick = { onCourseClick(course.id) },
onEnroll = { onEnrollClick(course.id) }
)
}
}
}
}
// ۳. کامپوزبل اختصاصی کارت هر دوره آموزشی
@Composable
fun CourseCardItem(
course: CourseUiModel,
onClick: () -> Unit,
onEnroll: () -> Unit,
modifier: Modifier = Modifier
) {
// انیمیشن نرم رنگ برای تغییر وضعیت ثبت نام
val containerColor by animateColorAsState(
targetValue = if (course.isEnrolled) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 350),
label = "card_color_animation"
)
Card(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onClick),
colors = CardDefaults.cardColors(containerColor = containerColor),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = course.title,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "مدرس: ${course.instructor}",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${course.priceTomans} تومان",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Button(
onClick = onEnroll,
enabled = !course.isEnrolled,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Default.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(if (course.isEnrolled) "ثبت نام شده" else "ثبت نام در دوره")
}
}
}
}
}
ناوبری مدرن و تایپ سیف (Type-Safe Navigation Compose) با کاتلین
یکی از نقاط ضعف نسخه های اولیه کامپوز، استفاده از رشته های متنی شکننده (String Routes نظیر "course_detail/{courseId}") برای جابجایی میان صفحات بود که کوچکترین اشتباه تایپی در نام پارامتر منجر به کرش غیرمنتظره در زمان اجرا میشد.
در سال ۲۰۲۶، گوگل سیستم Navigation Compose بر مبنای Type-Safety را به کمک کتابخانه رسمی Kotlinx Serialization معرفی کرده است. اکنون هر صفحه با یک شیء یا کلاس دادهای (Data Class / Object) تعریف میشود و پارامترها با تایپسیف کامل پاس داده میشوند:
import kotlinx.serialization.Serializable
import androidx.navigation.compose.*
import androidx.navigation.toRoute
// ۱. تعریف مقصدهای ناوبری به عنوان کلاس های Serializable
@Serializable
object CourseCatalogRoute
@Serializable
data class CourseDetailRoute(val courseId: Long, val initialTab: String = "overview")
// ۲. ساخت گراف ناوبری بدون رشته های متنی ناامن
@Composable
fun AppNavigation(navController: NavHostController) {
NavHost(navController = navController, startDestination = CourseCatalogRoute) {
composable<CourseCatalogRoute> {
CourseCatalogScreen(
onCourseClick = { id ->
navController.navigate(CourseDetailRoute(courseId = id))
}
)
}
composable<CourseDetailRoute> { backStackEntry ->
// استخراج مستقیم و امن داده بدون نیاز به پارس دستی رشته
val detailRoute: CourseDetailRoute = backStackEntry.toRoute()
CourseDetailScreen(courseId = detailRoute.courseId)
}
}
}
یکپارچگی جت پک کامپوز با Hilt، Room و Retrofit در معماری سازمانی
کامپوز به هیچ وجه یک ابزار ایزوله نیست، بلکه با تمام ستون های دیگر اکوسیستم مدرن اندروید ارتباطی ارگانیک دارد:
- تزریق وابستگی با Hilt: همانطور که در راهنمای جامع آموزش تزریق وابستگی با Hilt در اندروید تشریح شد، تنها با صدا زدن تابع
hiltViewModel()درون کامپوزبل، ویومدل با تمام وابستگی های اینجکتشده به صورت خودکار به اسکوپ اکتیویتی یا BackStackEntry متصل میشود. - دیتابیس محلی با Room: طبق اصول آموزش دیتابیس Room در اندروید، خروجی های کوئری دیتابیس به صورت
Flow<List<Entity>>واکشی شده و با یک خط کد در کامپوز به صورت بلادرنگ به وضعیت قابل رندر تبدیل میگردند. - ارتباط با وب سرویس با Retrofit: بر اساس متدولوژی آموزش رتروفیت Retrofit در اندروید، پاسخ های شبکه از طریق کورتین ها دریافت شده و خطاهای اتصال بدون افت فریم در کادرهای اسنکبار (Snackbar) متریال ۳ مدیریت میشوند.
تست نویسی جامع رابط کاربری در جت پک کامپوز: پایداری بدون نیاز به امولاتور فیزیکی
یکی از دردناکترین چالش های توسعه دهندگان اندروید در دوران سیستم سنتی XML، نوشتن تست های رابط کاربری با ابزار Espresso بود. تست های Espresso به دلیل ناهمگامی های زمانی در چرخه حیات (Timing Asynchrony)، انتظار برای انیمیشن ها و لزوم اجرای مداوم روی امولاتورهای سنگین، با خطاهای تصادفی مکرر (Flaky Tests) و کندی شدید فرآیند CI/CD همراه بودند.
جت پک کامپوز معماری تست رابط کاربری را از پایه بازطراحی کرده است. کامپوز به جای وابسته بودن به لایه های پیکسلی سیستم عامل، وضعیت عناصر را در یک درخت معنایی مستقل (Semantics Tree) نگه میدارد و از یک ساعت مجازی دقیق (Virtual Clock) برای کنترل بلادرنگ گذر زمان و انیمیشن ها بهره میبرد:
۱. معماری تست با ComposeTestRule
برای نوشتن تست های کامپوز، از قانون تست createComposeRule() استفاده میشود. این قانون به شما اجازه میدهد هر کامپوزبل منفرد را به صورت ایزوله بدون نیاز به راهاندازی کل اپلیکیشن رندر کرده و رفتارهای آن را با کدهای کاتلین زیر ذرهبین قرار دهید:
package ir.vahidhoseini.courses.presentation
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test
class CourseCatalogScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun courseCard_whenEnrolled_showsEnrolledStatusAndDisablesButton() {
// ۱. چیدمان داده های تستی و وضعیت اولیه (Arrange)
val testCourse = CourseUiModel(
id = 101L,
title = "آموزش پیشرفته جت پک کامپوز",
instructor = "وحید حسینی",
priceTomans = 2500000L,
isEnrolled = true
)
// ۲. رندر ایزوله کامپوننت مورد آزمون (Act)
composeTestRule.setContent {
CourseCardItem(
course = testCourse,
onClick = {},
onEnroll = {}
)
}
// ۳. اعتبارسنجی وضعیت دکمه و متون در درخت معنایی (Assert)
composeTestRule
.onNodeWithText("آموزش پیشرفته جت پک کامپوز")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("ثبت نام شده")
.assertIsDisplayed()
.assertIsNotEnabled()
}
@Test
fun courseCard_whenNotEnrolled_clickTriggersCallback() {
var enrollClicked = false
val testCourse = CourseUiModel(
id = 102L,
title = "معماری تمیز در کاتلین",
instructor = "وحید حسینی",
priceTomans = 1800000L,
isEnrolled = false
)
composeTestRule.setContent {
CourseCardItem(
course = testCourse,
onClick = {},
onEnroll = { enrollClicked = true }
)
}
// شبیه سازی کلیک کاربر روی دکمه ثبت نام
composeTestRule
.onNodeWithText("ثبت نام در دوره")
.performClick()
// راستی آزمایی فراخوانی رویداد
assert(enrollClicked)
}
}
با ترکیب createComposeRule() و فریم ورک محبوب Robolectric، میتوانید تمام تست های رابط کاربری جت پک کامپوز را مستقیماً روی ماشین مجازی جاوا (JVM محلی) بدون نیاز به بوت کردن شبیهسازهای سنگین اندروید اجرا کنید. این امر سرعت اجرای تست های UI را در خط لوله یکپارچه سازی مداوم (CI/CD) تا بیش از ۱۰ برابر افزایش میدهد.
استراتژی مهاجرت گام به گام از XML به جت پک کامپوز (Interoperability)
بسیاری از شرکت ها و تیم های نرم افزاری دارای اپلیکیشن های عظیم پروداکشن با صدها هزار خط کد XML و ده ها فرگمنت هستند. بازنویسی یکباره (Big Bang Rewrite) پروژه هایی پروداکشن همواره یکی از خطرناکترین تصمیمات فنی است که ریسک توقف توسعه قابلیت های تجاری و بروز باگ های پیشبینینشده را به همراه دارد. گوگل با آگاهی کامل از این دغدغه مهندسی، قابلیت سازگاری دوجانبه کامل (۱۰۰% Two-Way Interoperability) را در هسته کامپوز پیاده کرده است.
۱. تعبیه کدهای کامپوز درون لایوت های سنتی XML با ComposeView
شما میتوانید توسعه قابلیت های جدید یا کامپوننت های مستقل صفحه (مانند یک کارت محصول جدید، نوار جستجو یا لیست نظرات) را با کامپوز آغاز کرده و آن را مستقیماً درون یک فایل XML سنتی یا یک فرگمنت قرار دهید:
// تعبیه کامپوز درون یک Fragment سنتی به روش استاندارد
class ProductDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
// استراتژی چرخه حیات برای پیشگیری از نشت حافظه در فرگمنت
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme {
ProductReviewSection(productId = 1042L)
}
}
}
}
}
به طور پیشفرض، اگر ComposeView درون یک فرگمنت استفاده شود، ممکن است هنگام جابجایی بین تب ها چرخه حیات ویو نابود شده اما Composition باقی بماند که منجر به نشت حافظه میشود. همیشه از DisposeOnViewTreeLifecycleDestroyed استفاده کنید تا پاکسازی کامپوز دقیقاً با چرخه نابودی ویوی فرگمنت هماهنگ باشد.
۲. میزبانی ویوهای قدیمی در کامپوز با AndroidView
برعکس حالت فوق، اگر نیاز داشته باشید از یک کامپوننت سنتی که هنوز نسخه خالص کامپوزی آن موجود نیست (نظیر MapView شرکت گوگل، ویجت های نموداری شخص ثالث، یا WebView) درون صفحه کامپوز استفاده نمایید، از کامپوزبل AndroidView بهره میگیرید:
// استفاده از ویجت بومی و سنتی WebView درون درخت کامپوز
@Composable
fun WebTermsViewer(url: String, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = WebViewClient()
}
},
update = { webView ->
webView.loadUrl(url)
}
)
}
جدول مهندسی: مقایسه Jetpack Compose با سیستم سنتی XML و فریم ورک Flutter
برای انتخاب آگاهانه مسیر توسعه، ویژگی های فنی و معماری این سه ابزار برجسته را در سال ۲۰۲۶ مقایسه کنید:
| معیار مقایسه | جت پک کامپوز (Jetpack Compose) | سیستم سنتی XML (Android Views) | فریم ورک فلاتر (Flutter) |
|---|---|---|---|
| پارادایم طراحی UI | اعلانی مدرن (Declarative) | دستوری سنتی (Imperative) | اعلانی مدرن (Declarative) |
| زبان توسعه | کاتلین خالص (Kotlin) | ترکیب XML و کاتلین/جاوا | دارت (Dart) |
| تعداد کدهای پروژه | تا ۶۰٪ کمتر از XML | بسیار زیاد با فایل های متعدد واسط | متوسط |
| بهینه سازی Recomposition | Smart Skipping کامپایلری | ندارد (نیازمند مدیریت دستی View) | بازسازی درخت ویجت ها با Element Tree |
| دسترسی به قابلیت های نیتیو | ۱۰۰٪ مستقیم بدون Bridge | ۱۰۰٪ مستقیم و بومی | نیازمند Method Channels |
| پشتیبانی از چند سکویی (KMP) | کامپوز مالتیپلتفرم (Android, iOS, Desktop) | تنها در سیستم عامل اندروید | چند سکویی پیشفرض |
| پشتیبانی رسمی شرکت گوگل | استاندارد بلامنازع و آینده رسمی گوگل | حالت نگه داری (Maintenance Mode) | توسعه مداوم توسط تیم فلاتر گوگل |
امتیاز و نظرات
هنوز نظری ثبت نشده است. اولین نفر باشید.