کامپوز و کافه بازار

اعضای چپتر اندروید کافه‌بازار
اعضای چپتر اندروید کافه‌بازار


تقریبا ۲ سال پیش بودش که اولین نسخه استیبل کامپوز ریلیز شد (28 July 2021) و قبل از اون هم نسخه‌های آلفا از کامپوز در دسترس بودن و تو چپتر اندروید کافه بازار هم بحث‌های زیادی راجع به کامپوز بود و هست، تو جلسه‌های لول آپ چپتر راجع بهش صحبت میکردیم و حتی بچه‌های تیم یه سری سمپل‌های کوچک ازش زده بودن، اما تصمیم گیری راجع به مهاجرت به کامپوز تو اپلیکیشن کافه‌بازار به این سادگیا نبود و نیازمند بررسی‌های خیلی بیشتری بود؛ حدود ۴۳ تا فیچر ماژول و کلی کاستوم ویو کافی بود تا این درک رو داشته باشیم که پروژه خیلی بزرگ هستش و مهاجرت به این تکنولوژی جدید نیازمند برنامه ریزی دقیقی هستش و قاعدتا نیازمند زمان و انرژی برای اعضای تیم جهت یادگیری، همچنین تیم‌های محصولی باید قانع میشدن که چرا باید اندروید دولوپرهای تیمشون وقتشون رو برای این تکنولوژی صرف کنن که آورده‌ی محصولی‌ای نداره اما خوشبختانه چنین فرهنگی در شرکت و تیم‌ها بسیار زیاد هستش و علاقه بسیار زیاد به تکنولوژی‌های جدید در تمام افراد دیده میشه و برای کتابخانه‌ها و تکنولوژی‌های جدید دیگه هم که بهشون مهاجرت کردیم هیچوقت از این جنس مشکلات نداشتیم.

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

بازارچه - بازار‌من
بازارچه - بازار‌من


بعد از بازارچه کلی تجربیات جدید پیدا کردیم و چندتا محصول دیگه رو هم با کامپوز توسعه دادیم که در آینده احتمالا بیشتر راجع بهشون بشنویم و در حال حاضر در حال مهاجرت به کامپوز در اپلیکیشن بازار هستیم. امروز قصد داریم یه سری تجربیاتی که در این مسیر داشتیم رو باهم مرور کنیم و یه سری قوانین و تجربیات داخلی که تو کافه‌بازار برای کامپوز داریم و سعی میکنیم حواسمون بهش باشه رو باهم بررسی کنیم.


ریکامپوزیشن

ریکامپوزیشن کنترل نشده اتفاقی هستش که میتونه منجر به تجربه کاربری بد بشه برای کاربرانمون, در تعداد کم شاید چیزی حس نشه اما همواره باید بررسیش کنیم و تو app inspection حواسمون باشه که جایی که باید اسکیپ بشه به اشتباه ریکامپوز اتفاق نیفته، به این نکته توجه داشته باشیم که برای اسکیپ شدن باید تمام پارامترهای ورودی stable باشن و اینکه هیچ تغییری هم نکرده باشن (خیلی مهمه که بدونیم برای مقایسه و تشخیص تغییر از equal استفاده میشه و برای مثال دیتاکلاس‌ها equal رو پیاده سازی کردن و برای کلاس‌های دیگه باید خودمون پیاده سازی کنیم)، در غیر این صورت شاهد لگ و کند شدن اپلیکیشن و اتفاق‌های عجیب‌تر هستیم، در ادامه راجع به ریکامپوزیشن بیشتر صحبت میکنیم.


سعی کنیم تا جای ممکن Stable باشیم!

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

  • تمام پارامتر‌های ورودی Stable باشن (پریمیتیو تایپ‌ها مثل Boolean, Int, Long استیبل هستن)
  • برای دیتاکلاس‌هامون و هرجا که میتونیم از Immutable@ استفاده کنیم و اگه نیاز هست از Stable@ استفاده کنیم تا کامپایلر بتونه متوجه تغییرات بشه
  • از کالکشن‌های UnStable استفاده نکنیم, List و Map و Set تو کاتلین استیبل نیستن (با اینکه در باطن هستن, اما کامپایلر نمیتونه متوجهش بشه) به جاش از Kotlinx Immutable Collections استفاده کنیم, یعنی برای مثال به‌جای استفاده از List از ImmutableList استفاده کنیم.
  • حواسمون به لمبدا‌های unStable باشه, در ادامه بیشتر راجع بهشون صحبت میکنیم.


لمبدا‌‌های UnSatble!

کد زیر رو در نظر بگیرین:

123456789101112@Composable
fun RecompositionWithIssueTest(communicator: Communicator) {
    BazaarComposeLambdaTest(
        name = &quottest&quot,
        onNameClick = { communicator.onNameClick() },
    )
}

@Composable
fun BazaarComposeLambdaTest(name: String, onNameClick: () -> Unit) {
    Text(modifier = Modifier.clickable { onNameClick.invoke() }, text = name)
}

انتظاری که از BazaarComposeLambdaTest میره اینه که اگه با پارامتر name یکسان کال بشه اسکیپ میشه, اما این اتفاق نمیفته, چون هربار که این فانکشن کال میشه یه anonymous class برای اون لمبدا ساخته میشه که ممکنه Stable نباشه، کلاسی که برای این لمبدا ساخته میشه تو کانستراکتورش پارامتر‌هایی که اون لمبدا بهشون دسترسی داره رو دریافت میکنه، تو این مثال میشه Communicator و خب این کلاس با این فرض که stable نیست، لمبدای ما هم استیبل نیستش در نتیجه.

123456// communicator is unstable, so OnNameClickLambda is unstable too
class OnNameClickLambda(val communicator: Communicator) {
    operator fun invoke() {
        communicator.onNameClick()
    }
}

فقط توجه داشته باشیم که فانکشن‌های استاتیک و لمبدا‌هایی که از فیلدهای استیبل استفاده میکنن مشکلی ندارن، و برای حل اون لمبدا‌هایی که مشکل ریکامپوزیشن دارن چند راه داریم که در ادامه به آنها اشاره خواهیم کرد:

۱- لمبداها رو remember کنیم

1val onNameClick = remember(communicator) { { communicator.onNameClick() } }

۲- از Method References استفاده کنیم

1234567@Composable
fun RecompositionWithIssueTest(communicator: Communicator) {
    BazaarComposeLambdaTest(
        name = &quottest&quot,
        onNameClick = communicator::onNameClick()
    )
}


همواره Unstable هارو بررسی کنیم

برای اینکه متوجه بشیم کامپوزبل فانکشن‌هامون چقدر قابل اسکیپ شدن هستن و اگه جایی رو اشتباه کردیم مشکل از چی بوده، Compose Compiler Reports خیلی به کارمون میاد و گزارش‌هایی به شکل html برامون درست میکنه که تمام بررسی‌ها رو انجام داده و کامپوزبل فانکشن‌هایی که قابل اسکیپ شدن نیستند و پارامتر‌های unstable رو مشخص کرده؛ در تصویر پایین یه نمونشو میتونیم مشاهده کنیم.

با اینکه list در واقع immutable هستش اما stable نیست
با اینکه list در واقع immutable هستش اما stable نیست


استفاده از key در lazy layout ها

وقتی از lazy layout ها (LazyColumn و LazyRow و ...) استفاده میکنیم حتما حواسمون باشه که برای item ها از key استفاده کنیم, key بهمون کمک میکنه که از ریکامپوزیشن بیهوده وقتی یه آیتم اضافه میشه یا حذف میشه یا جابجا میشه تو لیست جلوگیری کنیم و فقط اون آیتمی ریکامپوزیشن براش اتفاق بیفته که تغییر کرده، روش پیشنهادی ما برای استفاده از key داخل آیتم‌هامون، یه اینترفیس سادست :

1234567interface ComposeItem {

   @Composable
    fun ComposeView()

    fun getItemId(metadata: String = &quot&quot): String
}

و برای استفاده ازش، تو آیتم‌هایی که قراره تو لیست‌ها نمایش داده بشن پیاده سازی میشه:

12345678@Immutable
data class HeaderInfoItem(
    private val title: String,
    private val body: String,
) : ComposeItem {
    override fun getItemId(metadata: String): String = &quot$title/$body/$metadata&quot

}

اینکه key چی باشه رو آیتم‌های صفحه با توجه به پارامتر‌هایی که دارن راجع بهش تصمیم میگیرن و metaData هم برای حالتی هستش که بخوایم از بیرون وقتی getItemId رو صدا میزنیم بهش پارامتری رو پاس بدیم (برای مثال میتونیم پوزیشن اون آیتم تو لیست رو بدیم) که در حالت دیفالت خالی هستش, این پیاده سازی بهمون کمک میکنه که وقتی یه آیتم واقعا تغییر کرد و نیاز به ریکامپوزیشن داشت براساس لاجیک آیتم‌های لیست و دیتایی که دارن، key تغییر کنه و ریکامپوزیشن اتفاق بیفته.

123456789101112@Composable
private fun Items(items: ImmutableList<ComposeItem>) {
    LazyColumn() {
        items(
            count = items.size,
            key = { index -> items[index].getItemId() },
        ) { index ->
            val item = items[index]
            item.ComposeView()
        }
    }
}


استفاده درست از CompositonLocal

یکی از قابلیت‌های خیلی خوب CompositonLocal ها هستن که فلسفه کلیشون دسترسی ساده‌تر بدون نیاز به دریافت اونها به عنوان پارامتر هستش، متریال این کارو خیلی زیاد و زیبا انجام داده به عنوان مثال وقتی از Color ها استفاده میکنیم.

1MaterialTheme.colorScheme.primary

اما باید توجه داشته باشیم که از CompositionLocal ها درست و درجای مناسب استفاده کنیم، به عنوان مثال باید توجه داشته باشیم که compositionLocal ها برای تمام فرزندها باید قابل استفاده باشد و به شکلی طراحی نشده باشند که کاراییشون فقط برای تعدادی از کامپوزبل فانکشن‌های فرزند باشد، همچنین باید توجه داشته باشیم که استفاده زیاد و اشتباه از CompositonLocal‌ ها باعث میشه خوانایی کد پایین بیاد و دیباگ کردن سخت‌تر بشه، در این قسمت از داکیومنت‌های رسمی به نکات بیشتری راجع به این موضوع اشاره شده است.

استیت‌ها و hoisting

در ابتدا یه مرور مختصر راجع به Stateless و Stateful داشته باشیم، به طور کلی اگر کامپوزبل فانکشن‌هامون استیتی رو داخل خودشون نگه دارن و تغییرش بدن یا به طور کلی تر از remember استفاده کرده باشن داخل خودشون، Stateful به حساب میان، در مواری که نیازی به کنترل از لایه‌های بالاتر نیست کاربردی هستن اما نباید فراموش کنیم که نگه داشتن استیت داخل کامپوزبل فانکشن‌ها باعث سخت شدن تستشون میشه و خواناییشونو کاهش میده، در برابر Stateful ها ما Stateless هارو داریم که هیچ استیتی داخل خودشون نگه نمیدارن و خوانایی خوبی دارن و همچنین تستشون کار ساده‌تری هست.

سوالی که به وجود میاد این هستش که چطور Stateless باشیم؟ جوابش خیلی سادست، با استفاده از state hoisting و به طور خلاصه این مفهوم که تا حد ممکن استیت‌ها و کال‌بک‌ها رو از لایه‌های بالاتر در ورودی کامپوزبل فانکشن‌هامون بگیریم.

یه سری نکات دیگه در رابطه با state ها وجود دارن که در ادامه به آنها میپردازیم:

  • از state فقط تو کامپوزبل فانکشن‌هامون استفاده کنیم و تو ویومدل از فلو‌ها استفاده کینم و از collectAsStateWithLifecycle استفاده کنیم برای تبدیل فلو به استیت
  • به کامپوزبل فانکشن‌هامون کوچک ترین چیزی که نیاز دارن رو پاس بدیم، به عنوان مثال اگه به یک لیست برای نمایش نیاز دارند، viewModel رو به عنوان پارامتر ورودی دریافت نکنیم، این کار باعث میشه راحت‌تر بتونیم اونها رو تو preview نمایش بدیم و تستشون ساده‌تر باشه و همچنین reusable باشن
  • از state به عنوان ورودی کامپوزبل فانکشن‌ها استفاده نکینم تا حد ممکن
  • وقتی از mutableStateOf داخل کامپوزبل فانکشن‌هامون استفاده میکنیم حواسمون باشه که اونها رو remember کنیم، در غیر این صورت با ریکامپوز شدن، استیتمون رو از دست میدیم


لگ‌ها و دراپ فریم‌ها رو دنبال کنید!

لگ زدن و دراپ فریم‌ها رو ممکنه همچنان شاهد باشیم! نکته‌ای که مهمه اینه که بتونیم پیداشون کنیم و یه آماری ازشون داشته باشیم و در نهایت فیکسشون کنیم، استفاده از پروفایلر میتونه یه گزینه خوب باشه برای تشخیص و رفعشون اما یه گزینه دیگه‌ای که وجود داره برامون که میتونیم باهاش پرفورمنس اپلیکیشن رو بررسی کنیم و اون استفاده از کتابخانه JankStats هستش، به کمک این کتابخانه میتونیم jank هارو پیدا کنیم و جزییات نسبتا دقیقی از اینکه کجا دارن اتفاق میفتن داشته باشیم و حتی میتونیم با یه سری از ابزارهای آنالیتیکس مثل سنتری ترکیبشون کنیم و تمام لگ‌ها و jank هایی که کاربرانمون تجربه میکنن رو در اختیار داشته باشیم و رفعشون کنیم، چیزی که در اختیارمون میزاره در نهایت یه همچین کال بکی هست:

12345678fun providesOnFrameListener(): JankStats.OnFrameListener {
    return JankStats.OnFrameListener { frameData ->
        if (frameData.isJank) {
            // A real app could do something more interesting, like writing the info to local storage and later on report it.
            Log.v(&quotJank&quot, frameData.toString())
        }
    }
}


لگ زدن و کند بودن اپلیکیشن در نسخه‌های دیباگ

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


سخن پایانی

تو این مقاله سعی کردیم توضیح مختصری از تجربه‌ کامپوز در چپتر اندروید کافه بازار بدیم و مروری کنیم رو تعدادی از قواعد کلی‌ و مهمی که سعی میکنیم رعایتشون کنیم و یک سری از تجربیاتی که داشتیم، این فرآیند همچنان در حال جلو رفتن هستش و حتما سعی میکنیم تجربیات جدیدمون رو باهاتون به اشتراک بزاریم. امیدواریم که این مقاله براتون مفید واقع شده باشه، همچنین اگه نیاز به پروژه‌ای داشتید که بتونید موارد بالا رو تمرین کنید میتونید به MusicAppComposeUI و Pheedly دوتا از پروژه‌های حمیدرضا صحرایی و PayPal از سعید مرادی یه نگاهی بندازید.