Composable در کامپوز :بررسی دقیق


وقتی تو اندروید با Jetpack Compose کار میکنی، همه چیز حول توابعی به اسم Composable @ میچرخه. یعنی به جای اینکه یه صفحه یا یه ویو رو مثل قدیم با XML و کلاسهای سنگین درست کنیم، یه تابع ساده مینویسیم که بگه چی باید نشون بده.
اما می خوایم بریم به عمق ماجرا ببینم این Composable چیه ! در سایت Developer Android یه اشاره های به کارکرد این تابع شده اما می خوایم یه خط داستانی دیگه ای رو پیش ببریم !
Composable@ یعنی چی؟ (تعریف ساده و دقیق)
یه علامت ، افزونه ، که به کامپایلر میگه: «این تابع یه بخش از رابط کاربر هست که میتونه خودش رو هر وقت لازم بود بهروز کنه و دوباره بکشه»
الان میخوام یه مثال از قدیم بزنم که ویو های قدیمی به چه صورت کار میکردند بعد مشکل اون ها رو بگیم
و بعد راه حل رو بگیم که میشه همون کامپوزی در دل این راه حل ساخته شد
در این جا از یه زاویه دیگه ای کامپوز بررسی شده :کامپوز چیه؟ فرق بین Compose و XML در اندروید
سیستم قدیمی View: چطور کار میکرد؟
سیستم های قدیمی بر این اساس کار میکرد ما با یه چیزی به اسم صفحه XML طراحی میکردیم بعد از این صفحه در منطق برنامه استفاده میکردیم
مثال :
این یه نمونه است که یک تکست ساده رو میخوایم نمایش بدیم
<TextView android:id="@+id/myText" android:text="سلام" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
بعد در قسمت منطق برنامه Logic که میشه در فرگمنت یا اکتیویتی ، از این TextView به این صورت استفاده میکردیم:
val textView = findViewById<TextView>(R.id.myText) textView.text = "خوش اومدی"
مشکل کجا بود ؟
در ظاهر رویه کار این طوری بود :
- باید XML بنویسی
- بعدش توی Kotlin یا Java بری اون ویو رو پیدا کنی
- اگه میخواستی محتوای ویو رو تغییر بدی، باید دستی انجامش بدی
- کلی کد تکراری و شلوغ
- ساختار کد دو تکه بود: یه تیکه XML، یه تیکه Kotlin
Jetpack Compose اومد و گفت :
داداش، بیا همه چی رو فقط با Kotlin بنویس! نه XML، نه findViewById! همه چی توی تابع، تمیز و جمعوجور ، خلاص !
و اونجا بود که Composable@ متولد شد!
اما همان طور که گفتم این ظاهر قضیه بود و شاید خیلی هامون با این موضوع مشکلی نداشتیم راحت هر تغییر که نیاز بود رو در سیستم قدیمی میدادیم .
اما توی باطن این ویوهای قدیمی چه اتفاقی می افتاد ؟
تبدیل XML به ویو واقعی در زمان اجرا
- XML فقط یه نقشه طراحی بود
اون فایل XML که مینوشتی، خودش یه کد نیست که اجرا بشه؛ بیشتر شبیه نقشهی ساختمونه. یعنی فقط میگه:
یه دکمه بذار، این اندازه باشه، فلان متن توش باشه، زیر فلان ویو باشه و…
اما این نقشه بهتنهایی هیچی نمایش نمی ده !
- در مرحله اجرا (runtime)، XML تبدیل میشه به Viewهای واقعی
وقتی اپ اجرا میشه، سیستم اندروید میاد:
- اون فایل XML رو تجزیه (parse) میکنه
- هر تگ (مثل TextView, Button, …) رو تبدیل میکنه به یه شیء (Object) از کلاس مربوطه در جاوا/کاتلین
- میذاره توی یه درخت ویو (View Hierarchy) که توی صفحه نمایش داده میشن
به عنوام مثال به این صورت :
تگ های XML :
<LinearLayout> <TextView /> <Button /> </LinearLayout>
توی حافظه تبدیل میشه به :
val layout = LinearLayout(context) val textView = TextView(context) val button = Button(context) layout.addView(textView) layout.addView(button)
اینا همشون توی یه درخت قرار میگیرن و سیستم گرافیکی اندروید میگه:
«خب، اینا باید رو صفحه دیده بشن»، و رسمشون میکنه.
حالا کاتلین در این ماجرا کجا بود ؟
وقتی ما می نوشتیم :
val tv = findViewById<TextView>(R.id.text1) tv.text = "سلام"
در واقع داشتیم به اون شیء View واقعی دسترسی پیدا میکردیم و خاصیتهاشو تغییر میدادیم.
این هم بگم که :
جنسشون فرق داره چون:
- XML یه فایل متنیه که تبدیل میشه به ویو
- Kotlin یه زبان برنامهنویسیه که اجرا میشه و با اون ویوها کار میکنه
در ادامه نیاز داریم مفهوم این درخت ویو رو بدونیم تا به مشکل اصلی پی ببریم !
ساخت View Tree و فرآیند Render در سیستم کلاسیک
View Tree (درخت ویو) چیه؟
تصور کن کل رابط کاربری یه صفحه، مثل یه درخته 🌳 ریشهی درخت، مثلاً LinearLayout یا ConstraintLayout هست و بچههاش میشن TextView, Button, ImageView, و…
هر ویو (View) توی اندروید، یه شیء (object) از یه کلاس خاصه و همه اینها با هم یه ساختار درختی میسازن که بهش میگن: View Hierarchy یا همون درخت ویو
چه زمانی این درخت ساخته میشه؟
- وقتی مینویسیم :setContentView(R.layout.activity_main)
- اندروید میره سراغ فایل XML activity_main.xml
- اون فایل رو “میخونه” و برای هر تگ، یه ویو میسازه:
-
<LinearLayout> <TextView /> <Button /> </LinearLayout>
- تبدیل میشه به:
val root = LinearLayout(context) val text = TextView(context) val btn = Button(context) root.addView(text) root.addView(btn)
- این درخت ساخته میشه و تحویل سیستم گرافیکی اندروید داده میشه
چطور نمایش داده میشه؟ (رندر شدن)
بعد از ساخته شدن درخت:
- اندروید میگه: خب، باید اینا رو بکشم روی صفحه
- از ویوی ریشه مثلا LinearLayout شروع میکنه:
- اول اندازه میگیره (measure) : ببینه هر ویو چقدر جا میخواد
- بعد مکان قرارگیری رو مشخص میکنه (layout)
- در آخر میکشه (draw) : یعنی با استفاده از Canvas همهچی رو رسم میکنه
این سه مرحله معروفن به : Measure -> Layout -> Draw
حالا اگه یه ()TextView داشتیم که میخواستیم رنگ اون رو عوض کنیم ، اندروید می اومد دوباره اون شی قبلی رو کامل پاک میکرد و دوباره رسم میکردم یعنی میرفت تو فاز Draw یا حتی چرخه دوباره تکرار میشد
حالا تا این جا چی میفهمیم از این توضیحات ، اینکه درخت ویو های قدیمی View Tree ، کلاسیک ، همه چی شی محور بود Object Oriented یعنی اگه چیزی تغییر کرد ، کلا پاکش کن ، تاکید میکنم کاملا، بعد دوباره از اول بیا اون رو بساز بعد ساخت شی و نگهداری و کنترل این ها یعنی از حافظه و منابع کار بکش ! اما یه راه داشت ، اگه خیلی دقیق مدیریت میکردی دیگه نیاز نبود ، ویو ها دوباره رسم بشه ! دقیق مدیریت کردن یعنی خون دل خوردن ، کار هر کسی نبود .
Jetpack Compose: ورود نسل جدید UI
این جا بود که Compose وارد شد و زمانی که Composable استفاده میکنیم دیگه با شی ها سر و کار نداریم این جا توابع برای ما این ویو ها رو میکشن ، چطوری ، !؟
مثال :
در سیستم کلاسیک (View System):
اگه صفحه گوشی رو یه صحنهی فیزیکی فرض کنیم، ویوها مثل:
- صندلیها، میزها، پنجرهها هستن
- ساخته میشن، یه جا قرار میگیرن
- میموندن تو صحنه
- وقتی میخواستی مثلاً متن رو عوض کنی، میرفتی سراغ اون صندلی و یه برچسب جدید روش میزدی
در سیستم کامپوز (Jetpack Compose):
تو انگار دیگه نمیری یه صندلی بذاری وسط صحنه
بلکه هر بار فقط میگی:
«الان صحنه این شکلی باشه»
دفعه بعد → «الان این شکلی باشه»
سیستم پشت صحنه خودش تصمیم میگیره:
- چی واقعاً باید ساخته بشه
- چی نیاز به حذف داره
- چی فقط آپدیت بشه
Compose چطور کار میکند؟ (برخورد تابعی به UI)
خیلی ساده:
- هنوز هم پشت صحنه View و Canvas وجود داره
- اما تو مستقیم با Viewها کار نمیکنی
- تو فقط یه توصیف (description) میدی از اینکه UI الان باید چطوری باشه
- Compose این توصیف رو میگیره و خودش تصمیم میگیره که:
- چی روی صفحه رسم بشه
- کدوم تکهها نیاز به تغییر دارن
- چقدر از رندر قبلی میتونه استفاده کنه
مثل یه کارگردان تئاتر هستی که نمیری خودت صندلی رو روی صحنه بذاری، فقط به بازیگرات میگی: ” تو الان بیا وسط، تو برو عقب ” ، کامپوز این جا دستیار ماست !و اونا خودشون اجرا میکنن
مزایای Compose در عملکرد و سرعت
الان یه سوال پیش میاد !؟
- قبلاً فقط ویو بود و من
- الان: من + دستور کلی (تعریف صحنه) + یه دستیار هوشمند به اسم Compose
پس انگار یه مرحلهی اضافه داریم. اما…
خب ، الان این که چیز دیگه هم اضافه شد ، پس چرا دارن میگن کامپوز سرعتش بالاست ؟!
سرعت اجرای بیشتر از کجا میاد؟
- حذف هزینههای قدیمی
در سیستم قدیمی:
- هر ویو (مثلاً TextView, Button) یه شیء بزرگ جاوا/کاتلین بود
- کلی property داشت حتی اگه استفاده نمیکردی (مثل: background, padding, elevation, accessibility info, …)
مثلاً حتی یه TextView ساده چندین کیلوبایت حافظه میگرفت!
امادر Compose:
- هیچ شیء دائمیای ساخته نمیشه، فقط خروجی تابع رسم میشه
- فقط وقتی لازمه یه Node ساده (و سبکتر) ساخته میشه
یعنی حافظه کمتر مصرف میشه، ویو سبکتره، سرعت بیشتره.
- فقط همون قسمتهای تغییر کرده دوباره رسم میشن
در View قدیمی:
- اگه یه متن عوض شه، ممکنه کل TextView یا حتی کل Layout دوباره Measure + Layout + Draw شه
در Compose:
- سیستم فقط میفهمه که “آها! این متن عوض شد”
- پس فقط همون تابع Text() که بهش دادهاش عوض شده، دوباره اجرا میشه
- بقیهی صحنه اصلاً دست نمیخوره
یعنی کار کمتر = CPU کمتر = سریعتر
- حذف Virtual Layout Pass اضافی
- در View کلاسیک:
- سیستم برای محاسبه اندازهها و جاگیریها چندین بار درخت View رو بالا پایین میرفت (Measure → Layout → Draw)
- در Compose:
- سیستم ساختار سبکی داره که با استفاده از Slot Table و حافظهی ساختاریافته (Structured memory) این کارو خیلی سریعتر انجام میده
سیستم ردیابی تغییرات در Compose
سیستم ردیابی Compose چطور کار میکنه؟
یعنی این سوال:
«چطور Compose میفهمه فقط فلان قسمت UI باید دوباره ساخته بشه؟»
جواب:
با استفاده از یک ساختار سبک و بهینهشده به نام:
- Slot Table
- Snapshot State
- Recomposer
Slot Table چیه؟
یه جدول داخلی که هر بار کامپوزر اجرا میشه، اطلاعات مهم هر تابع @Composable رو توش ذخیره میکنه. مثل:
- کدوم تابع اجرا شد (مثلاً Text(“Ali”))
- چه پارامترهایی داشت
- آیا تغییر کرده یا نه
بهکمک این جدول، Compose میتونه بررسی کنه که آیا یه تابع باید دوباره اجرا شه یا نه.
پس نیازی نیست کل UI رو بررسی کنه، فقط میره سراغ بخشهایی که احتمال تغییر دارن.
Snapshot State چیه؟
هر بار که مینویسی:
var name by remember { mutableStateOf("Ali") }
یه سیستم ردیاب روی این مقدار فعال میشه.
اگه مقدارش عوض بشه، Compose خودکار میفهمه که فقط اون تابعی که به این مقدار وابسته بود باید دوباره اجرا شه. نه بیشتر، نه کمتر.
Recomposer چیکار میکنه؟
وظیفهی اصلیش اینه که:
- ببینه چی تغییر کرده
- تعیین کنه کدوم Composable باید دوباره اجرا شه
- اجرای مجدد اون تیکهها رو مدیریت کنه
جمعبندی: چرا Compose یک تغییر بنیادین است؟
نتیجه:
- نیازی به پاک کردن و ساختن مجدد نیست
- فقط همون تیکههایی که تغییر کردن، دوباره ساخته میشن
- حافظه کمتر استفاده میشه
- سرعت بیشتر میشه
این همون چیزیه که سیستم قدیمی نداشت و برای همین Compose خیلی سبکتر و سریعتره.
در یک جمله:
در سیستم قدیمی:
«اگه چیزی تغییر کرد، کلاً پاکش کن و از نو بساز»
در Compose:
«فقط همون قسمت کوچیکی که تغییر کرده رو دوباره اجرا کن»
امیدوارم از این مطالب استفاده کنید و از مسیر کد زدن لذت ببرید 🍕