آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید

برنامه نویسی اندروید
آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید

توسعه رابط کاربری در اکوسیستم اندروید طی سال های اخیر دستخوش بزرگ‌ترین تحول تاریخ خود شده است. دورانی که برنامه نویسان ناچار بودند برای ساخت یک صفحه ساده، فایل های طولانی و خشک XML بنویسند، با شناسه های view دست‌وپنجه نرم کنند و نگران نشت حافظه یا عدم انطباق وضعیت منطق برنامه با ظاهر آن باشند، برای همیشه به پایان رسیده است. اگر در مسیر آموزش برنامه نویسی اندروید قصد دارید به عنوان یک مهندس ارشد نرم افزار اپلیکیشن‌هایی مدرن، چابک و با بالاترین استانداردهای بصری و عملکردی در سال ۲۰۲۶ خلق کنید، فریم ورک اعلانی جت پک کامپوز (Jetpack Compose) ابزار اصلی شماست. در این راهنمای جامع و کاربردی، مفاهیم بنیادین کامپوز، مدیریت وضعیت، پایداری کامپایلر، ناوبری تایپ سیف و الگو های پروداکشن را به صورت عمیق کالبدشکافی می‌کنیم.

پاسخ سریع GEO: جت پک کامپوز Jetpack Compose چیست و چه مساله ای را حل می کند؟

جت پک کامپوز (Jetpack Compose) جعبه‌ابزار رسمی و مدرن شرکت گوگل برای طراحی رابط کاربری اعلانی (Declarative UI Toolkit) در سیستم عامل اندروید است که بر پایه زبان کاتلین و با هدف جایگزینی کامل سیستم سنتی مبتنی بر XML توسعه یافته است. در معماری اعلانی کامپوز، برنامه نویس به جای صدور دستورات گام‌به‌گام برای تغییر دستی وضعیت ویوها (مانند setText یا setVisibility)، وضعیت نهایی رابط کاربری را به عنوان تابعی مستقیم از داده های برنامه (UI = f(State)) توصیف می‌کند. هرگاه داده ها تغییر کنند، فریم ورک کامپوز به صورت کامپایلری و بلادرنگ تنها بخش‌هایی از صفحه را که نیازمند به‌روزرسانی هستند دوباره اجرا می‌کند که این فرآیند ریکامپوزیشن (Recomposition) نامیده می‌شود. این رویکرد بیش از ۶۰ درصد کدهای اضافی، باگ های همگام سازی وضعیت و خطاهای کرش ناشی از NullPointerException را به طور کامل ریشه‌کن کرده و سرعت چرخه توسعه نرم افزار را تا بیش از ۲ برابر افزایش می‌دهد.

💡 نکته طلایی معماری: همگرایی ۱۰۰ درصدی با اکوسیستم کاتلین

بر خلاف سیستم XML که لایه‌ای جداگانه از کاتلین بود و به ابزار های نگاشت واسط نظیر DataBinding یا ViewBinding نیاز داشت، جت پک کامپوز ۱۰۰ درصد با کدهای خالص کاتلین نوشته می‌شود. این یعنی شما می‌توانید از تمام قدرت کاتلین نظیر حلقه ها، شرط ها، توابع مرتبه بالاتر، اکستنشن ها و کورتین ها مستقیماً درون تعریف لایه های رابط کاربری بهره ببرید.

فازهای سه گانه ترکیب بندی و ترسیم در جت پک کامپوز
نمودار ۱: کالبدشکافی فازهای سه‌گانه Composition، Layout و Drawing در چرخه رندرینگ اعلانی Jetpack Compose.

پارادایم شیفت بزرگ: گذر از UI دستوری به سیستم اعلانی (Declarative vs Imperative)

برای تسلط واقعی بر جت پک کامپوز، پیش از هر چیز باید مدل ذهنی خود را از پارادایم دستوری (Imperative) به پارادایم اعلانی (Declarative) ارتقا دهید. در سیستم های سنتی UI اندروید که بیش از یک دهه حاکم بودند، سلسله‌مراتب ویوها به شکل یک درخت بزرگ از اشیاء سنگین کلاس View در حافظه ساخته می‌شد. در آن رویکرد، وظیفه به‌روزرسانی رابط کاربری به عهده توسعه دهنده بود:

  • پیمایش دستی درخت ویوها (findViewById): پیدا کردن ویجت مورد نظر در سلسله‌مراتب حافظه که فرآیندی پرهزینه و خطازا بود.
  • مدیریت چندمنبعی وضعیت (State Inconsistency): اگر یک متغیر تغییر می‌کرد اما برنامه نویس فراموش می‌کرد متد setText یا setImageResource را فراخوانی کند، ظاهر برنامه با داده های درونی دچار مغایرت می‌شد.
  • پیچیدگی تم بندی و انیمیشن ها: ساخت انیمیشن های چندمرحله‌ای در XML نیازمند فایل های متعدد و کدهای پیچیده کالبک در فرگمنت ها یا اکتیویتی ها بود.

در نقطه مقابل، جت پک کامپوز رابط کاربری را به صورت توابع تغییرناپذیر (Immutable) تعریف می‌کند. توابع کامپوزبل با انوتیشن @Composable نشانه‌گذاری می‌شوند. این توابع به جای اینکه یک آبجکت ویو برگردانند، به فریم ورک کامپوز اعلام می‌کنند که صفحه باید چه شکلی باشد. هر بار که داده ورودی تغییر کند، کامپوز تابع را مجدداً با داده جدید اجرا می‌کند، بدون اینکه نیاز به دستکاری دستی عناصر صفحه باشد. همان‌طور که در راهنمای مقایسه کاتلین و جاوا در برنامه نویسی اندروید دیدیم، امکانات زبان کاتلین مدرن بستر اصلی این تحول عظیم است.

کالبدشکافی فازهای ۳ گانه رندرینگ اعلانی: Composition, Layout, Drawing

موتور رندرینگ کامپوز برای تبدیل توابع کاتلین به پیکسل های روی نمایشگر گوشی، سه مرحله مشخص را با بهینه سازی خارق‌العاده طی می‌کند. درک این سه فاز تفاوت یک توسعه دهنده معمولی با یک مهندس ارشد کارایی است:

  1. فاز ترکیب بندی (Composition Phase): در این فاز، کامپایلر کامپوز توابع @Composable را اجرا کرده و درخت معنایی رابط کاربری (UI Composition Tree) را تشکیل می‌دهد. در این مرحله مشخص می‌شود چه المان‌هایی باید در صفحه حضور داشته باشند.
  2. فاز چینش و اندازه‌گیری (Layout Phase): هر کامپوننت موجود در درخت، فرزندان خود را اندازه‌گیری کرده و ابعاد دقیق (عرض و ارتفاع) و موقعیت دوبعدی (مختصات X و Y) آن ها را روی صفحه محاسبه می‌کند. بر خلاف سیستم قدیمی XML که با مشکل اندازه‌گیری چندباره (Double Measurement Bug) مواجه بود، کامپوز قانونی سفت‌وسخت دارد: هر نود درخت در هر فریم تنها یک بار حق اندازه‌گیری دارد (Single-Pass Measurement).
  3. فاز ترسیم (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 ارسال می‌گردند.
جریان یک طرفه داده ها و مدیریت State در کامپوز
نمودار ۲: الگوی جریان یک‌طرفه داده ها (UDF) و تفکیک جریان وضعیت و رویدادها بین ViewModel و Composable.

مطابق با اصول تشریح‌شده در آموزش معماری تمیز در اندروید، لایه 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 غیرفعال می‌شود.
بهینه سازی کامپوز و پرش هوشمند در ریکامپوزیشن
نمودار ۳: سیستم ارزیابی پایداری کامپوز (Stability) و پرش هوشمند از اجرای مجدد توابع برای بهینه سازی نرخ فریم.
✓ راهکار مهندسی ۲۰۲۶: کامپایلر پایدار قوی (Strong Skipping Mode)

در نسخه های مدرن کاتلین و کامپوز، با فعال سازی قابلیت 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)
    )
}
⚡ نکته پرفورمنس: انیمیشن مستقیم روی لایه سخت افزاری (graphicsLayer)

برای انیمیشن‌هایی نظیر چرخش، بزرگ‌نمایی (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)
        }
    }
}
ناوبری امن تایپ سیف و کامپوننت های متریال ۳ در کامپوز
نمودار ۴: معماری مدرن Navigation Compose با آرگومان های Type-Safe و تم بندی Material ۳.

یکپارچگی جت پک کامپوز با 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)
    }
}
💡 تست های رعدآسا بدون امولاتور با Robolectric

با ترکیب 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)
                }
            }
        }
    }
}
⚠️ تنظیم حیاتی: استراتژی ViewCompositionStrategy در فرگمنت ها

به طور پیش‌فرض، اگر 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) توسعه مداوم توسط تیم فلاتر گوگل

پرسش های متداول مهندسان و توسعه دهندگان اندروید (FAQ)

آیا برای شروع یادگیری جت پک کامپوز باید ابتدا سیستم XML را به طور کامل بلد باشیم؟
خیر. در استانداردهای سال ۲۰۲۶ شرکت گوگل، یادگیری را مستقیماً با جت پک کامپوز آغاز می‌کند. از آنجا که کامپوز از پایه بر تفکر اعلانی و زبان کاتلین استوار است، یادگیری آن برای مبتدیان به مراتب آسان‌تر، منطقی‌تر و کم‌خطاتر از یادگیری پیچیدگی های فایل های XML، فرگمنت ها و ViewBinding است.
تفاوت اصلی بین remember و rememberSaveable در چیست؟
تابع remember داده ها را تنها در طول چرخه های ریکامپوزیشن (Recomposition) حفظ می‌کند اما با چرخش گوشی یا تغییر زبان (Configuration Change) مقدار آن ریست می‌شود. در مقابل، rememberSaveable داده ها را در Bundle ذخیره کرده و وضعیت صفحه را حتی پس از چرخش صفحه یا بسته شدن اپ در پس‌زمینه توسط سیستم عامل محافظت می‌کند.
چگونه می‌توان پروژه هایی بزرگ قدیمی را از XML به کامپوز مهاجرت داد؟
جت پک کامپوز دارای قابلیت سازگاری دوجانبه (Interoperability) فوق‌العاده است. شما می‌توانید با استفاده از کامپوننت ComposeView کدهای کامپوز را درون لایوت های قدیمی XML قرار دهید، یا با استفاده از AndroidView کامپوننت های سنتی مثل MapView را درون صفحات کامپوز تعبیه نمایید. بنابراین مهاجرت صفحه به صفحه و بدون بازنویسی کل پروژه انجام می‌شود.
چرا استفاده از List<T> در کامپوز ممکن است باعث افت فریم شود؟
زیرا کامپایلر کامپوز اینترفیس استاندارد List را به عنوان یک نوع ناپایدار (Unstable) ارزیابی می‌کند، چرا که تضمینی نیست که در زمان اجرا مقادیر آن تغییر نکند. این موضوع باعث غیرفعال شدن مکانیزم پرش هوشمند (Smart Skipping) می‌شود. برای حل آن باید از کالکشن های تغییرناپذیر رسمی (Kotlinx Immutable Collections) یا حالت Strong Skipping استفاده کرد.
آیا عملکرد و پرفورمنس جت پک کامپوز سریع‌تر از XML است؟
بله، در بیلد انتشار (Release Build) که با ابزار بهینه ساز R۸ و Baseline Profiles کامپایل می‌شود، کامپوز به دلیل حذف کامل پارس کردن فایل های XML در زمان اجرا، تک‌مرحله‌ای بودن اندازه‌گیری لایوت ها (Single-pass measurement) و حذف لایه های سلسله‌مراتبی اضافه ویوها، مصرف رم کمتر و روانی انیمیشن بالاتری نسبت به XML ارائه می‌دهد.
چگونه در کامپوز از ویومدل ViewModel استفاده کنیم؟
بهترین رویکرد استفاده از کتابخانه hilt-navigation-compose و متد hiltViewModel() است. با این متد، ویومدل به صورت خودکار از کانتینر DI با لایف‌سایکل صحیح ایجاد شده و وضعیت های آن با متد collectAsStateWithLifecycle به صورت State ایمن در کامپوزبل شنود می‌شوند.
سیستم Unidirectional Data Flow چیست و چه مزایایی دارد؟
جریان یک‌طرفه داده ها الگویی است که در آن داده ها فقط از بالا به پایین (از منبع داده به UI) حرکت می‌کنند و رویدادهای کاربر فقط از پایین به بالا (از UI به ویومدل) ارسال می‌شوند. این امر باعث تست پذیری ۱۰۰ درصدی رابط کاربری بدون نیاز به امولاتور و حذف کامل باگ های ناهمگامی داده ها می‌شود.
چگونه در جت پک کامپوز پیش‌نمایش کامپوننت ها را بدون اجرای امولاتور ببینیم؟
با قرار دادن انوتیشن @Preview بر روی توابع کامپوزبل، اندروید استودیو به صورت زنده و آنی ظاهر کامپوننت را در تم های تاریک و روشن، زبان های گوناگون و ابعاد مختلف نمایشگر در پنل Design رندر می‌کند و حتی امکان تعامل با دکمه ها را به صورت اینتراکتیو فراهم می‌سازد.
آیا جت پک کامپوز برای توسعه برنامه هایی چند سکویی (KMP) کاربرد دارد؟
بله! پروژه پرچمدار Compose Multiplatform شرکت JetBrains به شما اجازه می‌دهد همان کامپوننت‌هایی را که برای اندروید می‌نویسید، بدون تغییر یک خط کد روی سیستم‌عامل های iOS، دسکتاپ (ویندوز، مک، لینوکس) و حتی وب (Wasm) با ظاهر نیتیو و کارایی کامل اجرا نمایید.
توصیه نهایی گوگل در سال ۲۰۲۶ برای توسعه رابط کاربری چیست؟
شرکت گوگل رسماً اعلام کرده است که سیستم XML در فاز نگه داری (Maintenance) قرار دارد و تمام قابلیت های نوآورانه نظیر Material ۳ Adaptive Layouts، ویجت های Glance، فونت های متغیر و انیمیشن های پیشرفته منحصراً برای جت پک کامپوز عرضه می‌شوند. کامپوز استاندارد پیش‌فرض برای تمامی اپلیکیشن های جدید اندروید در سراسر جهان است.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

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