5 ترفند Jetpack Compose که ای کاش زودتر میدانستم (برای توسعهدهندگان اندروید)


در این مقاله، 5 ترفند Jetpack Compose را بررسی میکنیم که Jetpack Compose یک ابزار فوقالعاده است، اما ویژگیهای خاص و گاهی عجیب آن میتواند حتی توسعهدهندگان باتجربه را شگفتزده کند.
این مقاله را مانند یک تور پشتصحنهی Compose در نظر بگیرید: در مورد الگوهای کارآمد طراحی UIکه باعث بهینهسازی Compose می شوند ، بهترین شیوههای مدیریت وضعیت (state)، ابزارهای قدرتمند Android Studio و موارد دیگر صحبت خواهیم کرد. در پایان، شما به مجموعهای از نکات حرفهای مجهز خواهید شد که باعث روانتر شدن اپلیکیشنها و تمیزتر شدن کدهای شما میشوند.
با remember کارهای پرهزینه را کش کنید ، ساده ترین ترفند Jetpack Compose
یکی از سادهترین راهها و اولین ترفند Jetpack Compose در این مقاله ، برای بهبود عملکرد این است که از تکرار کارهای سنگین هنگام بهروزرسانی رابط کاربری جلوگیری کنید. در Jetpack Compose، یک composable ممکن است در هر فریم (frame) دوباره اجرا شود اگر ورودیهایش تغییر کرده باشند.
اگر داخل یک composable محاسبات سنگینی انجام دهید، آن محاسبات ممکن است در هر بازترکیب (recomposition) مجدداً اجرا شوند و باعث کندی UI شوند و این بهینهسازی Compose را تحت تاثیر قرار میدهد. برای جلوگیری از این مشکل، از remember استفاده کنید تا نتیجهی آن محاسبات کش شده و فقط یکبار محاسبه شود.
برای مثال، فرض کنید میخواهید یک لیست از مخاطبین را قبل از نمایش مرتبسازی کنید:
@Composable
fun ContactList(contacts: List<Contact>, comparator: Comparator<Contact>) {
// Remember the sorted list so sorting only happens when 'contacts' or 'comparator' change
val sortedContacts = remember(contacts, comparator) {
contacts.sortedWith(comparator)
}
LazyColumn {
items(sortedContacts) { contact ->
Text(text = contact.name)
}
}
}
در این کد، استفاده از remember(contacts, comparator) { … } باعث میشود که مرتبسازی فقط زمانی انجام شود که لیست contacts یا تابع comparator تغییر کند. بدون remember، حتی با اسکرول سادهی لیست (که باعث بازترکیب آیتمهای جدید میشود)، لیست در هر بار دوباره مرتبسازی میشود — که این کاملاً ناکارآمد است.
نکتهی حرفهای
این الگو فقط برای محاسبات سنگین نیست — بلکه برای مقادیر مشتقشده (derived state) هم کاربرد دارد.« در ادامه به این مفهوم کامل میپردازم» به عنوان مثال، فرض کنید موقعیت اسکرول یک لیست را دارید و فقط زمانی میخواهید عملی انجام دهید که اسکرول از یک مقدار خاص عبور کند. در این حالت، میتوانید مقدار مورد نظر را با derivedStateOf مشتق کنید.
در کل، انتقال محاسبات از داخل بدنهی Composable به لامبداهای remember شده، باعث میشود رابط کاربری شما از سربارهای غیرضروری جدا شود — و این به معنای کد تمیزتر و عملکرد بهتر است.
Hoist کردن State و جریان دادهی یکجهته برای UI قابل پیشبینی
Jetpack Compose زمانی بهترین عملکرد را دارد که کامپوننتهای رابط کاربری شما بدون حالت (stateless) باشند. اما یکی از اشتباهات رایج بین تازهکارها این است که مشخص نیست چه کسی مالک state است.
State Hoisting یک الگوی مهم در Compose است که میگوید:
«وضعیت را به سطح بالاتر منتقل کن و کامپوننت composable فقط وضعیت را دریافت کرده و تغییرات را از طریق callback اطلاع دهد»
این کار باعث میشود:
- فقط یک منبع واحد برای وضعیت وجود داشته باشد (Single Source of Truth)، که مدیریت داده و تستپذیری را سادهتر میکند.
- دادهها فقط به سمت پایین جریان داشته باشند (props) و رویدادها فقط به بالا برگردند (callbacks).
این همان جریان دادهی یکجهته (Unidirectional Data Flow) است که به کدهای تمیزتر، قابل تستتر، و قابل پیشبینیتر منجر میشود.
اگه در مورد این قضیه(state , stateful , hoisting , . . .) ، اطلاعاتی نداری پیشنهاد میکنم این مطلب رو بخونی : مفهوم State در Compose
مثلاً به جای این کامپوننت stateful:
@Composable fun HelloContent() { var name by rememberSaveable { mutableStateOf("") } TextField( value = name, onValueChange = { new -> name = new }, label = { Text("Name") } ) }
از این نسخهی hoisted استفاده کن:
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent( name = name, onNameChange = { new -> name = new } ) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { TextField( value = name, onValueChange = onNameChange, label = { Text("Name") } ) }
این الگو به شما کمک میکند ساختار اپلیکیشنتان تمیزتر شود، کنترل کاملی روی وضعیت داشته باشید، و راحتتر تست بنویسید
در این مثال، HelloContent فقط مقدار name را نمایش میدهد و هنگام تغییر متن، تابع onNameChange را صدا میزند — اما خودش مالک MutableState نیست. این یعنی مالکیت وضعیت (state ownership) در جای دیگری (مثلاً HelloScreen) قرار دارد.
این الگوی hoisting باعث میشود که جریان داده بسیار واضح و قابل پیگیری باشد:
- داده از بالا به پایین منتقل میشود (name)
- رویدادها از پایین به بالا ارسال میشوند (onNameChange)
نتیجهاش؟ کدی تمیزتر، قابل تستتر، و راحتتر برای درک و نگهداری.
نکته حرفهای :
رابط کاربری خود را به عنوان یک تابع خالص در نظر بگیر:
«ورودی میگیرد، خروجی تولید میکند»
نه اینکه مخفیانه درون خود وضعیت ذخیره کند!
برای مدیریت وضعیتهایی State که باید در برابر مرگ پروسه (process death) یا تغییر کانفیگ (مثل چرخش صفحه) مقاوم باشند، میتوانی از ابزارهایی مثل:
- rememberSaveable
- ViewModel
استفاده کنی ، در معماری ایدهآل، stateشما داخل ViewModel یا Repository قرار دارد، و Composable فقط آن را مشاهده (observe) میکند و بهروزرسانی میشود — بدون اینکه خودش چیزی را نگه دارد.
کاهش بازترکیبها: استفاده از derivedStateOf و کلیدهای پایدار (stable keys)
در Jetpack Compose، هر زمان که یک Composable ، یک state را بخواند، و آن state تغییر کند، آن Composable دوباره ترکیب (recompose) میشود. اما گاهی اوقات، وضعیت بهطور مکرر تغییر میکند، در حالی که فقط در شرایط خاصی میخواهیم بازترکیب انجام شود. و این دومین ترفند Jetpack Compose در این مقاله است .
اینجاست که derivedStateOf میدرخشد ✨ — این ابزار به شما اجازه میدهد مقداری مشتقشده از وضعیت را محاسبه کنید، اما فقط زمانی بازترکیب انجام شود که مقدار مشتقشده تغییر کند، نه هر بار که وضعیت اولیه تغییر میکند.
توضیح DerivedStateOf:
در ابتدای کار برای این که ، مفهوم خوب متوجه بشیم نیاز داریم ، که معنی مشتق رو بدونیم :
✅مشتقشده یعنی چی؟
از روی یه چیز دیگه محاسبه شده یا نتیجهگیری شده باشه
🟢 مثال سادهی روزمره:
فرض کن یه فروشگاه داری.
- قیمت واحد کالا = 10 هزار تومان
- تعداد خرید شده = 3
خب حالا:
- جمع کل = قیمت × تعداد = 30 هزار تومان
اینجا جمع کل یه مقدار مشتقشده از قیمت و تعداد هست.
یعنی خودش مستقل نیست، بلکه بر اساس دو تا داده دیگه محاسبه شده.
🟦 حالا مثال در Compose :
فرض کن:
val count = remember { mutableStateOf(3) }
و یه مقدار جدید تعریف میکنی که بگه آیا count بیشتر از 5 هست یا نه:
val isHigh = count.value > 5
اینجا isHigh یک مقدار مشتقشده از count هست.
یعنی خودش state نیست، ولی وابسته به یه state دیگه است و با تغییر اون دوباره محاسبه میشه.
مثال عملی: دکمهی “اسکرول به بالا“
برای مثال، تصور کن یک دکمه “اسکرول به بالا” (Scroll to Top) داریم که فقط وقتی ظاهر شود که اندیس اولین آیتم قابل مشاهده (first visible item index) بیشتر از ۰ باشد. ممکن است از یک LazyListState برای پیگیری وضعیت لیست استفاده کنی. به جای اینکه بنویسی:
val showButton = remember { mutableStateOf(false) } LaunchedEffect(listState.firstVisibleItemIndex) { showButton.value = (listState.firstVisibleItemIndex > 0) }
در این حالت در هر بار که اسکرول انجام میشه و هر بار که شماره اندیس بالا می ره و این تابع دوباره باز سازی میشه ، حالا
انجام این کار سادهتر می شود اگر به صورت زیر استفاده کنید :
@Composable fun ShowScrollToTop(listState: LazyListState) { // derivedStateOf recalculates when index changes, but recomposes only if the Boolean actually flips val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } if (showButton) { Button(onClick = { /* scroll to top */ }) { Text("Scroll to Top") } } }
در اینجا، derivedStateOf { … } یک state ایجاد میکند که هر زمان firstVisibleItemIndex تغییر کند،
یعنی DerriveState of دوباره ارزیابی میشود، اما کامپوز تنها زمانی ShowScrollToTop را بازترکیب (recompose) میکند که مقدار showButton واقعاً تغییر کند یعنی
«فقط در زمانی که مقداراندییس از صفر به 1 تغییر کند اما در بقیه موارد ،اگر اندیس زیاد بشه مانند این که از 1 به 2 یا از 3 به 4 و الی آخر DeriveStateof می فهمد که نیاز نیست تغییری انجام دهد » برای بازترکیب نادیده گرفته میشوند.
نکتهی دیگر:
همیشه در لیستها یا هنگام استفاده از remember(key)، از کلیدهای پایدار (stable keys) استفاده کن. برای مثال، استفاده از items(list, key = { it.id }) در LazyColumn به کامپوز کمک میکند تا هویت آیتمها را تشخیص دهد و از بازترکیب کامل جلوگیری کند. و اگر از remember(someKey) استفاده میکنی، مطمئن شو که someKey واقعاً بازتابدهندهی زمانی است که میخواهی مقدار ذخیرهشده تازه شود.
نکته حرفهای:
هر زمان که استیتی داری که از یک استیت دیگر مشتق شده و کمتر تغییر میکند، از derivedStateOf استفاده کن. این در اصل مانند یک “تشخیصدهنده تغییر” عمل میکند تا UI فقط در مواقع لازم دوباره اجرا شود. این الگو در کدهای بهینهی Compose رایج است؛ و اگر با مدلهای دادهای خوب (مثلاً کلاسهای دادهای غیرقابلتغییر) همراه شود، بررسی برابری استیتها سریع و درست انجام میشود.
ساختار سادهتر، عملکرد بهتر: طراحی Layout سبک با اجتناب از تو در تویی زیاد
یکی از اشتباهات رایج در Jetpack Compose اینه که UI رو با ترکیب زیاد از Column, Row, Box به شکل لایهلایه و تو در تو مینویسیم. این کار باعث میشه ساختار UI پیچیده و سنگین بشه؛ بهطوری که زمان ترکیب و حافظه مصرفی بالا بره و عملکرد برنامه پایین بیاد.
اما راهحل چیه؟
Compose ابزارهایی داره که دقیقاً برای جلوگیری از این مشکل طراحی شدن:
استفاده از Layoutهای Lazy مثل:
- LazyColumn
- LazyRow
- LazyVerticalGrid
و… این Layoutها فقط آیتمهایی رو که روی صفحه دیده میشن ترکیب میکنن — نه کل لیست رو! این یعنی حافظه و زمان ترکیب بهطرز چشمگیری کاهش پیدا میکنه.
نمونه اشتباه (تودرتویی و ترکیب کامل):
Column { items.forEach { item -> Text(item.name) } }
در اینجا حتی اگه فقط ۳ آیتم روی صفحه دیده بشه، همهی لیست یکجا ترکیب میشه!
روش بهینه با LazyColumn:
LazyColumn(modifier = Modifier.fillMaxSize()) { items(itemsList) { item -> Text(text = item.title) } }
فقط آیتمهایی که قابل مشاهده هستن ترکیب میشن، و بقیه موقع اسکرول شدن تولید میشن — این یعنی بازدهی بالا و مصرف پایینتر منابع.
راهکارهای بیشتر برای طراحی ساده و کارآمد:
- از تودرتویی زیاد اجتناب کن. مثلاً بهجای Column { Row { Box { Text(…) } } } سعی کن با استفاده از Modifierها همون نتیجه رو با لایههای کمتر بگیری.
- هر لایهی اضافی (یک Box، Row یا Column بدون نیاز واقعی) یعنی بار بیشتر برای کامپوز.
- برای لیستهای کوتاه هم LazyColumn گزینهی امن و بهینهتریه.
- از Modifierهایی مثل padding, Arrangement, Alignment, weight به جای لایهسازی اضافی استفاده کن.
- اگه ساختار UI مسطح و ساده باشه، نگهداری راحتتر و عملکرد روانتری خواهی داشت.
آموزش Arrangment , Alignment در Compose
نکته حرفهای:
- از ابزارهایی مثل Layout Inspector استفاده کن تا ببینی کجاها UI عمیق یا پیچیده شده.
- اگر تصویر، اندازهگیری یا بخشی از UI قابل بازاستفادهست، از remember یا painterResource استفاده کن تا از محاسبه یا بارگذاری تکراری جلوگیری بشه.
- همیشه یادت باشه: هر کاری رو فقط زمانی انجام بده که واقعاً لازمه.
پیشنمایشها و بازرسی را در Android Studio به اوج برسان
Jetpack Compose ابزارهای قدرتمندی در Android Studio دارد. با امکانات خاص Compose در این IDE آشنا شو:
Preview @
کافیست کامپوزابلها را با این annotation علامت بزنی تا بدون اجرای اپ، خروجی UI را در IDE ببینی.
میتوانی چندین پیشنمایش برای حالتها یا دستگاههای مختلف بسازی.
@Preview(name = "Light Mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun GreetingPreview() { MaterialTheme { Greeting("Compose") } }
پیشنمایشها در Jetpack Compose به صورت زنده و همزمان با کد زدن شما بهروزرسانی میشوند و این یعنی دیگر نیازی به اجرای مکرر امولاتور نیست!
میتوانید در annotation ویژگیهایی مثل دستگاه، عرض (widthDp)، ارتفاع (heightDp) یا تمها را مشخص کنید.
مثلاً:
@Preview(device = "id:pixel_4", showBackground = true)
به شما اجازه میدهد خروجی را به شکل یک دستگاه Pixel 4 شبیهسازی کنید.
حالت Focus Mode در تب طراحی (Design) این امکان را میدهد که یک پیشنمایش را به صورت ایزوله شده ببینید.
حالت تعاملی (Interactive Mode):
نسخههای جدید Android Studio به شما اجازه میدهند در پیشنمایش تعامل کنید؛ مثلاً روی دکمهها کلیک کنید یا بین تبها جابهجا شوید تا رفتار کاربر را شبیهسازی کنید.
این قابلیت کمک میکند مشکلات UI را خیلی زود و بدون نصب برنامه روی دستگاه یا امولاتور تشخیص دهید.
Layout Inspector:
زمانی که اپ روی دستگاه یا امولاتور اجرا میشود، با باز کردن Layout Inspector > Compose Inspector میتوانید
- ساختار درختی کامپوزها را ببینید
- تعداد بازترکیبها (recomposition counts) را مشاهده کنید
- محلهای بازترکیب را هایلایت کنید (مثلاً اگر یک کامپوننت بیش از حد بازترکیب میشود، با گرادیان رنگی نمایش داده میشود)
- پارامترها و اطلاعات معنایی (semantic info) هر نود را ببینید که بسیار برای دیباگ کاربردی است.
Compose Profiler:
در بخش System Trace اندروید استودیو یک قسمت مخصوص Compose وجود دارد که
- زمان فریمها،
- زمانهای اندازهگیری (measure)،
- چیدمان (layout)،
- رسم (draw)،
- و تعداد بازترکیبها را در هر فریم نشان میدهد.
این ابزار برای پیدا کردن کندی یا مشکلات عملکردی بسیار مفید است.
Pro Insight:
همیشه کامپوزابلهایت را در پیکربندیهای مختلف (حالت تاریک، صفحهنمایش کوچک، راست به چپ) در پیشنمایشها بررسی کن.
یک بار با کمک Compose Inspector متوجه شدم یک باگ بازترکیب پرهزینه به خاطر رنگهای گرادیانی ناخواسته در Layout Inspector آشکار شد!
ابزارهای توسعه معمولا تفاوت یک برنامهنویس خوب و عالی Compose را مشخص میکنند.💻
معماری تمیز با ViewModel و StateFlow
در پسزمینه، UI در Compose فقط یک نمایشگر داده است. برای ساخت اپلیکیشنهای مقاوم و قابل نگهداری، معماریهای استاندارد مثل MVVM یا MVI را با کتابخانههای Jetpack استفاده کن.
الگوی رایج:
- ViewModel به همراه StateFlow یا ( LiveData)برای نگهداری وضعیت UI
- سپس در Compose این وضعیت ها State را جمعآوری (collect) کن.
// In a ViewModel class MainViewModel : ViewModel() { private val _uiState = MutableStateFlow("Hello, Jetpack Compose!") val uiState: StateFlow<String> = _uiState } // In Composable @Composable fun MainScreen(viewModel: MainViewModel = viewModel()) { val text by viewModel.uiState.collectAsState() // subscribe to StateFlow Text(text = text) }
در اینجا یک StateFlow از ViewModel منتشر میشود. در داخل MainScreen، فراخوانی collectAsState() آن را به یک State<String> در Compose تبدیل میکند و بهطور خودکار هنگام تغییر مقدار، رابط کاربری را دوباره رسم میکند. استفاده از viewModel() تضمین میکند که ViewModel از تغییر پیکربندیهای UI تاثیر نپذیرد و بهدرستی scope شود. این کار منطق تجاری و بهروزرسانی state را از لایهی UI جدا نگه میدارد.
راهنمای رسمی Jetpack Compose این الگو را اینگونه توضیح میدهد:
«StateFlow تضمین میکند که UI به صورت واکنشی هنگام تغییر دادهها بهروزرسانی شود و viewModel() به صورت خودکار ViewModel را در بازترکیبها نگه میدارد.»
با جمعآوری وضعیت به صورت State در Compose، شما به طور ذاتی جریان داده یکطرفه (Unidirectional Data Flow) را دنبال میکنید؛ یعنی ViewModel وضعیت جدید را به پایین میفرستد و UI رویدادها را به سمت بالا ارسال میکند.
سوال: همیشه باید از StateFlow به جای LiveData استفاده کنم؟
پاسخ:
StateFlow راهکار مبتنی بر Coroutine کاتلین است و به خوبی با Compose کار میکند. امکانات بیشتری دارد و نیاز به lifecycle owner داخل Composables ندارد.
LiveData همچنان کار میکند (میتوانید از observeAsState() استفاده کنید)، اما StateFlow سبکتر و هماهنگتر با Coroutine هاست.
انتخاب به نیاز تیم و پروژه شما بستگی دارد، اما مهم است که روی یک الگو پایدار بمانید.
نکته معماری:
سازماندهی کد به لایههای جداگانه بسیار به صرفه است.
مثلاً یک الگوی معمول، تعریف یک Holder وضعیت UI (مثلاً یک data class یا sealed class) است که تمام فیلدهای UI را توصیف میکند، همراه با متدهایی در ViewModel برای بهروزرسانی این وضعیت.
در این حالت، کامپوزابلها فقط وضعیت را دریافت و نمایش میدهند و مستقیماً آن را تغییر نمیدهند. این جداسازی کار تست را بسیار آسانتر میکند.
نکته حرفه ای :
برای UIهای بسیار پیچیده، مدل MVI را در نظر بگیر:
- یک UiState واحد داشته باش
- از UI به ViewModel با ارسال Intentها یا Actionها کار کن
این الگو انتقالهای حالت را کنترلشده و غیرقابل تغییر (immutable) نگه میدارد.
به هر حال، همیشه کامپوزابلها را Stateless نگه دار و منطق کسبوکار را در ViewModel یا Use-Caseها بنویس.