قسمت اول:
نکته 1: سعی کنید لغت های تخصصی رو که قرار دادم بلد باشید.
نکته 2: هرجایی که جمله نامفهوم بود یا کلمه ای اشتباه املایی داشت(پیش میاد!) حتما با من درمیون بذارین تا اصلاحش کنم.
کامپوز یه kit توسعه رابط کاربریِ declarative هستش که پیاده سازی رابط کاربری و نگهداری از اون رو برای شما راحت میکنه.
توی سیستم UI قبلی اندروید شما یه درختی از view داشتین که وقتی وضعیت(state) برنامه تغییر میکرد بدلیل مثلا تعامل کاربر، این سلسله مراتب UI هم باید update بشه در پاسخ به تعامل کاربر، پروسه update اینجوری بود که میرفت کل درخت UI رو میگشت با استفاده از متد findViewById و node ها رو با صدا زدن اینا عوض میکرد.
button.setText(String)
container.addChild(View)
img.setImageBitmap(Bitmap)
این متدها(همونایی که bold کردم) میان وضعیت داخلی(internal state) ویجت رو تغییر میدن و خییلی راحته که شما یادتون بره مثلا یک ویجت setText کنین و متن رو آپدیت کنین یا اصلا یه node رو تنظیم میکنین روی یک ویجت و اون ویجت هم کلا از درخت UI قبلا حذف شده بوده. بطور کلی پیچیدگیِ نگهداریِ نرمافزار وقتی رشد میکنه که تعداد زیادی view نیاز به آپدیت داشته باشن.
لغت node: زیرمجموعه یه view که میتونه تغییر کنه، همون text و image و color و ... منظورشه.
این پارادایم تا حد زیادی طراحی رابط کاربری رو ساده تر میکنه. اینطوری کار میکنه که بجای اینکه کل سلسله مراتب UI بگرده که کدوم view آپدیت شده، میاد کل صفحه رو از اول render میکنه و فقط اون تغییرات موردنیاز رو اعمال میکنه. این رویکرد نمیاد مثل UI سیستم سنتی بیاد getter/setter تعریف کنه، بلکه با یه سری فانکشن بنام composable میاد UI رو تعریف میکنه و تغییرات یا داده ها رو بعنوان پارامتر به اینا پاس میدیم.
نکته: ما قبلا به عناصر رابط کاربری مون میگفتیم View، ولی الان به هرچی که view میگفتیم، میگیم Widget(ویجت) و این ویجت ها رو هم با composable فانکشن ها تعریف میکنیم.
نکته: قواعد نامگذاری اش هم بصورت PascalCase هستش.
گفتیم که رابط کاربری مون رو با مجموعه ای از فانکشن های composable تعریف میکنه. اینا data رو بعنوان پارامتر ورودی میگیرن و عناصر رابط کاربری رو از تغییرات باخبر میکنن. یه نگاه به فانکشن Greeting بندازید:
میبینید که این فانکشن یه string میگیره و یه ویجت text ساتِع(emit) میکنه.
1- همه composable فانکشن ها باید annotation @composable رو داشته باشن؛ این فانکشن به compiler کامپوز میگه که این فانکشن برای دادن داده ها به رابط کاربریه.
2- این فانکشن یه متن رو نشون میده تو UI با استفاده از composable فانکشنِ Text(). فانکشن های composable سلسله مراتب UI رو با استفاده از call کردن composable فانکشن های دیگه میسازند؛ مثل اینجا که ما یه composable فانکشن ساختیم و توش از یه composable فانکش دیگه استفاده کردیم.
3- این فانکشن ها چیزی رو return نمی کنن بلکه عنصر های UI رو ساتع یا emit میکنند.
4- این فانکشن ها سریع، idempotent و side-effect free(البته اگه شما side-effect ای درست نکنین، تو پست های بعضی دربارش حرف میزنیم) هستن.
* این فانکشن ها اگه صد بار هم با همون آرگومنت صدا زده بشن باید یک رفتار رو داشته باشن مگر اینکه از طریق global variable هایی از خارج فانکشن(side-effect) رفتار تابع رو تغییر بدنن.
توی سیستم UI سنتی، هر ویجتی یه internal state خودش رو داره و با getter و setter میتونیم اون state اش رو عوض کنیم(اصطلاحا بهش میگن stateful)
ولی توی رویکرد declarative کامپوز ویجت ها stateless(استیتی ندارن) اند و هیچ getter و setter ندارن و وقتی هم میخواین state شون رو عوض کنین بجای set کردن مثلا text میاین اون داده رو تو آرگمانِ فانکشنِ کامپوز قرار میدین.
میبینید که (مثلا) از viewmodel یه سری داده ها اومده و داده ها از composable های بالاتر میرسه به پایینتر، اونایی که داده لازم دارن(البته تو آرگومان هاشون) .
وقتی که state تغییر کرد، فانکشن composable دوباره صدا زده میشه با داده جدید که سبب میشه عناصر رابط کاربری دوباره کشیده بشن رو صفحه، به این پروسه میگن recomposition.
چون فانکشن های composable با کاتلین نوشته شدند، شما میتونین از هر ویژگی ای که کاتلین داره تو پیاده سازی UI و فانکشن های composable تون استفاده کنید. مثلا میتونین از حلقه ها، عبارت های شرطی، عملگرها و .. استفاده کنید:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
فریمورک کامپوز بصورت هوشمندانه فقط اون اجزایی رو recompose میکنه که آرگومانشون تغییر کرده.
برای مثال این فانکشن رو با یه button درنظر بگیرین:
@Composable fun ClickCounter(clicks: Int, : () -> Unit) { Button( = ) { Text("I've been clicked $clicks times") } }
هر بار که روی این دکمه کلیک میشه، فرض کنین مقدار clicks هم آپدیت بشه، اون وقت compose میاد این lambda با فانکشن Text که داره دوباره با داده جدید صدا میزنه که گفتیم بهش میگن recomposition و فانکشن های دیگه که به مقدار clicks وابسته نیستن رو recompose نمیکنه یعنی اون فانکشن هارو نادیده میگیره(skip میکنه).
یه نکته ای رو هم که باید درنظر داشته باشین درباره فانکشن های کامپوز اینه که این فانکشن ها باید side-effect نداشته باشن(تو پست های بعدی درباره این مفصل صحبت میکنیم). Side-effect تعاریف مختلفی داره از افراد مختلف، ولی تو این مورد: به هر تغییراتی که state بیرونِ فانکشن (composable) رو تغییر بِده، میگیم side effect. اگه side effect داشته باشین، ممکنه توی پروسه recomposition، اونی که میخواین skip بشه، نشه(پست های بعدی).
فانکشن های composable باید تو هر فریم دوباره اجرا باشن، پس باید عملکردشون سریع باشه تا وقتی که مثلا یه انیمیشن اجرا میشه تو هر فریم لَگ نزنه و سریع اونو render کنه، پس اگه کارهایی دارین که ممکنه زمانبر باشه برید تو background انجامش بدید.
اگه به کد زیر نگاهی بندازین، احتمالا فکر کنید این فانکشن ها به ترتیب اجرا میشن، ولی لزوما اینطور نیست و کامپوز ممکنه اینا رو تو هر ترتیبی اجرا کنه. Compose تشخیص میده کدوم فانکشن اولویت بالاتری داره و اونو اول اجرا میکنه. مثلا یه checkbox ای که هنوز visible نیست تو صفحه رو میندازه تو اولویت آخر قاعدتا که خیلی performance رو بهبود میده.
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
یه مثال دیگه: فرض کنید compose اول StartScreen اجرا کرد اما وسط اجرا کردنش یهو میبینه که این فانکشن وابسته است(side effect) به MiddleScreen، اون وقت compose قاعدتا میره و MiddleScreen رو اجرا میکنه.
کامپوز میتونه recomposition رو با اجرا کردن composable ها بصورت موازی بهینه کنه. تو این مورد، compose میاد از core های CPU دستگاه کاربر استفاده میکنه و composable ها رو بصورت موزای با استخری از background thread اجرا میکنه.
و برای صدمین بار نباید تو composable فانکشن ها side effect داشته باشیم چون چیزایی که مربوط به UI هستش رو حتما باید توی UI thread اجرا بشه اگه تو background thread اجرا بشه برنامتون crash میکنه.
کامپوز فقط اون بخش هایی رو recompose میکنه که state شون(آرگومان هاشون) عوض شده باشه. مثلا اگه یه state یه button تغییر کنه فقط اون recompose میشه، دیگه کاری به قبل و بعدش نداره. برای مثال:
/** * Display a list of names the user can click with a header */ @Composable fun NamePicker( header: String, names: List<String>, onNameClicked: (String) -> Unit ) { Column { // this will recompose when [header] changes, but not when [names] changes Text(header, style = MaterialTheme.typography.bodyLarge) Divider() // LazyColumn is the Compose version of a RecyclerView. // The lambda passed to items() is similar to a RecyclerView.ViewHolder. LazyColumn { items(names) { name -> // When an item's [name] updates, the adapter for that item // will recompose. This will not recompose when [header] changes NamePickerItem(name, onNameClicked) } } } } /** * Display a single name the user can click. */ @Composable private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { Text(name, Modifier.clickable( = { onClicked(name) })) }
هرکدوم از اینا ممکنه skip بشه توسط کامپوز اگه ورودی شون تغییر نکنه.
وقتی پارامتر یه composable تغییر کرد، compose اونو recomose میکنه. این خوش بینانه بودنش یعنی compose انتظار داره قبل از اینکه پارامتر دیگه ای تغییر کنه، recomposition رو انجام بده؛ اگه قبل از تموم شدن recomposition یه پارامتر از یه composable تغییر کنه، اونوقت compose میاد recomposition رو متوقف میکنه و recomposition رو دوباره شروع میکنه با پارامتر جدیده.
اینجا هم اگه side effect ای داشته باشین، مشکل براتون پیش میاد. مثلا، فرض کنین که تو پروسه recomposition هستیم و یه composable یه side effect داره، حالا اگه compose بیاد recomposition رو متوقف کنه اون side effect هنوز داره اجرا میشه و فاجعه رخ میده.
گفتم که مثلا یه فانکشنی برای اجرای هر فریم از یه انیمیشن دارید، به تعداد فریم ها، همونقدر هم فانکنش composable صدا زده میشه و اگه وظیفه سنگینی مثل خوندن از storage رو به این فانکشن واگذار کنین، احتمالا به خطای not responding بَر بخورید.
این قسمت اول بود. امیدوارم خوشتون اومده باشه. اشکالات، ابهامات و نظراتتون رو تو کامنت ها به اشتراک بذارین.
ایمیل: cornerdroid@gmail.com