Recomposition چیه و چطور نذاریم آپ مون کند بشه


رازهایی برای بهبود عملکرد اپلیکیشنهای Jetpack Compose با کاهش Recomposition های غیرضروری و ساخت رابط کاربری سریعتر و روانتر
Jetpack Compose چیه؟ Jetpack Compose ابزار مدرن اندرویده برای ساخت رابط کاربری بهصورت نیتیو (Native). یعنی دیگه لازم نیست کلی کد XML بنویسی، چون با استفاده از این ابزار و زبان کاتلین، خیلی راحتتر و سریعتر میتونی رابط کاربری بسازی.
چرا باید Recomposition رو بهینه کنیم؟
بهینه کردن Recomposition خیلی مهمه، چون اگه درست مدیریت نشه باعث میشه اپت کند بشه، سیپییو زیاد درگیر بشه و حتی باتری گوشی زودتر خالی شه. پس با کم کردن Recompositionهای بیمورد، میتونی یه تجربه روانتر و اپ بهینهتری ارائه بدی.
Recomposition رو بهتر بشناسیم.
Recomposition چیه؟ وقتی توی Jetpack Compose، وضعیت (state) یه چیزی تغییر میکنه، سیستم میاد اون قسمت از UI رو که به اون وضعیت وابستهست دوباره میسازه یا همون Recompose میکنه.
چه وقتهایی Recomposition اتفاق میافته؟
- وقتی یه وضعیت (state) که توی یه تابع @Composable استفاده شده تغییر کنه
- وقتی پارامترهای یه تابع @Composable تغییر کنن
- یا وقتی یه لامبدا یا تابع رو بهعنوان پارامتر پاس بدی و اون عوض بشه
چطور Recompositionهای بیخود رو پیدا کنیم؟
ابزارهایی مثل Android Studio Profiler خیلی کمک میکنن. همچنین میتونی از لاگنویسی (Log) یا ابزار Recompose Highlighter استفاده کنی تا بفهمی کدوم قسمتها داره زیادی Recompose میشه.
منابع رایج Recompositionهای غیرضروری
مثالهایی که در ادامه هست خیلی سادهان و فقط برای توضیح دادن موضوعه. توی پروژههای واقعی ما معمولا کلی المان توی UI داریم، ولی برای درک بهتر، سادهاش کردیم.
۱. استفاده بیش از حد از توابع @Composable
مشکل: اگه رابط کاربری رو بیشازحد به تکههای کوچیک تقسیم کنی (مثلاً برای هر متن و عکس یه تابع جدا بسازی)، ممکنه باعث بشه با هر تغییر کوچیک، چندین قسمت دوباره Recompose بشن، حتی اگه لازم نباشه.
مثال ساده :
@Composable
fun UserProfile() {
Column {
UserAvatar()
UserName()
UserBio()
}
}
@Composable
fun UserAvatar() {
Image(painter = painterResource(R.drawable.avatar), contentDescription = "User Avatar")
}
@Composable
fun UserName() {
Text(text = "John Doe")
}
@Composable
fun UserBio() {
Text(text = "Lorem ipsum dolor sit amet.")
}
توی این مثال، هر بخش پروفایل کاربر یه تابع جداست. این از نظر نظم کدنویسی خوبه، ولی اگه بهینه مدیریت نشه، ممکنه با هر تغییر کوچیکی کل اینا دوباره رسم بشن.
راهحل: اجزای مرتبط رو با هم نگهدار و از remember و rememberUpdatedState استفاده کن تا فقط در صورت نیاز Recompose بشن.
نسخه بهینهشده
@Composable
fun UserProfile() {
val avatarPainter = remember { painterResource(R.drawable.avatar) }
val userName = remember { "John Doe" }
val userBio = remember { "Lorem ipsum dolor sit amet." }
Column {
Image(painter = avatarPainter, contentDescription = "User Avatar")
Text(text = userName)
Text(text = userBio)
}
}
اینجا چون از remember استفاده کردیم، مقدارهای عکس و متن فقط یکبار ساخته میشن و تا وقتی تغییر نکنن، دیگه Recompose نمیشن.
۲. مدیریت اشتباه وضعیت (State)
مشکل: اگه وضعیت رو درست مدیریت نکنی، ممکنه باعث شه کل UI با یه تغییر کوچیک دوباره Recompose بشه، در حالی که فقط یه قسمت باید آپدیت بشه.
مثال :
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
// Other UI components that don't need to recompose on count change
}
}
اینجا ما داریم از یک mutableStateOf استفاده میکنیم تا مقدار count رو نگه داریم. مشکلی که هست اینه:
🔁 هر بار که کاربر روی دکمهی Increment کلیک میکنه، مقدار count تغییر میکنه
🔁 تغییر count باعث Recomposition کل تابع Counter() میشه
⚠️ و چون کل تابع Counter() دوباره اجرا میشه، تمام اجزای داخل Column هم دوباره رسم میش حتی اونهایی که هیچ ربطی به count ندارن!
حتی اگه مثلاً ۲۰ تا المان داخل
Columnباشه که هیچ کاری بهcountندارن، باز هم همهشون دوباره اجرا میشن. این یعنی مصرف منابع بیشتر، فشار بیشتر به CPU، کندی احتمالی UI.
اینجا با هر بار کلیک روی دکمه، کل Column دوباره رسم میشه. این توی رابطهای پیچیده مشکلزاست.
راهحل: قسمتهایی از UI رو که وابسته به وضعیت هستن جدا کن.
نسخه بهینهشده:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
CountDisplay(count)
IncrementButton { count++ }
// Other UI components that don't recompose
}
}
@Composable
fun CountDisplay(count: Int) {
Text(text = "Count: $count")
}
@Composable
fun IncrementButton(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Increment")
}
}
تو این نسخه، ما CountDisplay و IncrementButton رو از Counter جدا کردیم. چی میشه اینجا؟:
🟢 وقتی count تغییر میکنه، فقط CountDisplay(count) دوباره Recompose میشه.
🟢 چون IncrementButton هیچ وابستگیای به count نداره، اصلاً نیازی نیست دوباره اجرا بشه، پس Compose هم اون رو دوباره رسم نمیکنه.
🟢 سایر اجزای UI که داخل Column هستن ولی ربطی به count ندارن، هم در امان میمونن.
🧠 پشت صحنه: Compose چطوری تصمیم میگیره چی رو Recompose کنه؟
Jetpack Compose به صورت اتوماتیک بررسی میکنه که کدوم Composableها از چه Stateهایی استفاده کردن. وقتی یه State تغییر میکنه، فقط اون Composableهایی که از اون State استفاده کرده بودن، وارد فرآیند Recomposition میشن.
مثلاً:
-
CountDisplay(count)چونcountرو به عنوان پارامتر گرفته، Compose میفهمه که این تابع بهcountوابستهست. -
IncrementButtonچون هیچ دادهای ازcountنداره (فقط یهonClickداره)، هیچ دلیلی نداره که دوباره Recompose بشه.
۳. استفاده نادرست از متغیرهای وضعیتدار (Mutable State)
مشکل: اگه بیرویه از mutableStateOf استفاده کنی یا ساختار وضعیتت بهینه نباشه، میتونه باعث Recompositionهای اضافی بشه.
مثال :
@Composable
fun UserProfile() {
var user by remember { mutableStateOf(User("John Doe", "Lorem ipsum dolor sit amet.")) }
Column {
Text(text = user.name)
Text(text = user.bio)
Button(onClick = { user = user.copy(name = "Jane Doe") }) {
Text("Change Name")
}
}
}
data class User(val name: String, val bio: String)
اینجا وقتی اسم کاربر عوض میشه، کل Column دوباره Recompose میشه، در حالی که فقط یه Text باید آپدیت بشه.
راهحل: وضعیتها رو به قسمتهای کوچیکتر تقسیم کن و از rememberUpdatedState برای مدیریت بهتر استفاده کن.
نسخه بهینهشده :
@Composable
fun UserProfile() {
val userName = remember { mutableStateOf("John Doe") }
val userBio = remember { "Lorem ipsum dolor sit amet." }
Column {
Text(text = userName.value)
Text(text = userBio)
Button(onClick = { userName.value = "Jane Doe" }) {
Text("Change Name")
}
}
}
@Composable
fun UserBio(bio: String) {
Text(text = bio)
}
با جدا کردن userName و userBio، فقط متن اسم کاربر موقع تغییر Recompose میشه، نه کل UI.
نتیجه گیری و توضیح مثال ها : در مثال UserProfile()، ما یک آبجکت User رو با mutableStateOf نگه داشتیم. با اینکه فقط name رو تغییر دادیم، اما چون از user.copy(name = "...") استفاده کردیم، کل آبجکت جدید ساخته شد. در نتیجه، Compose متوجه میشه که مقدار state تغییر کرده و کل تابع UserProfile() رو Recompose میکنه. دلیلش هم اینه که این تابع در اجرای قبلی از user استفاده کرده، پس به اون state وابسته است؛ حتی اگه user به عنوان پارامتر ورودی به تابع داده نشده باشه. این یعنی در Compose، هر تابعی که در زمان اجرا از یک state استفاده کنه، به اون وابسته میشه و با تغییرش دوباره اجرا میگرده. پس بهتره وضعیتها رو به شکل کوچکتر و جداگانه نگهداری کنیم تا Recomposition فقط برای قسمتهای لازم اتفاق بیفته و از رندر بیمورد کل UI جلوگیری بشه.
جمعبندی: چطور رابط کاربری روان و بهینه بسازیم؟
بهینهسازی Recomposition توی Jetpack Compose یه نکته خیلی مهمه واسه ساخت اپلیکیشنهایی که هم سریع باشن و هم مصرف منابعشون پایین باشه. چند نکتهی مهم که باید همیشه یادت بمونه:
۱. اول بفهم Recomposition چیه و چه وقتهایی اتفاق میافته
۲. از ابزارهایی مثل Profiler و Log استفاده کن تا بفهمی کجاها زیاد Recompose میشن
۳. توابع @Composable رو به اندازه تقسیم کن، نه زیاد ریز نه زیاد درشت. از remember برای مدیریت وضعیت استفاده کن
۴. وضعیتها رو به درستی مدیریت کن؛ بهتره هر بخش UI فقط به همون وضعیت مربوط خودش واکنش نشون بده
اگه این تکنیکها رو خوب یاد بگیری، میتونی قدرت کامل Jetpack Compose رو به کار بگیری و اپهایی بسازی که هم کاربر ازش لذت ببره و هم خودت با کد تمیز حال کنی.
اگه میخوای بیشتر یاد بگیری، حتماً مستندات رسمی Jetpack Compose رو هم یه نگاهی بنداز و با آپدیتهای جدید دنیای اندروید همراه باش.
موفق باشی و کد زدنات پر از لذت 😎







