رفع مشکلات پایداری

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

فعال کردن رد شدن قوی

حالت پرش قوی به‌طور پیش‌فرض از Kotlin نسخه 2.0.20 فعال است. حالت پرش قوی به تابع‌های ترکیب‌پذیر با پارامترهای ناپایدار اجازه می‌دهد با مقایسه پارامترهای ناپایدار بااستفاده از برابری نمونه (===) نادیده گرفته شوند، درحالی‌که پارامترهای پایدار همچنان بااستفاده از برابری ساختاری (Object.equals()) مقایسه می‌شوند.

اگر از نسخه Kotlin پایین‌تر از ۲.۰.۲۰ استفاده می‌کنید، ابتدا حالت پرش قوی را فعال کنید. اگر حالت پرش قوی ازقبل فعال شده است و یک composable هنوز به‌طور غیرضروری بازترکیب می‌شود، بررسی کنید که آیا نمونه‌های جدیدی از پارامترهای ناپایدار در هر گذر بازترکیب اختصاص داده می‌شوند یا نه.

برای اطلاعات بیشتر، رد شدن قوی را ببینید.

تغییرناپذیر کردن کلاس

همچنین می‌توانید یک کلاس ناپایدار را کاملاً تغییرناپذیر کنید تا کامپایلر بتواند پایداری آن را به‌طور خودکار استنباط کند.

  • تغییرناپذیر: نوعی را نشان می‌دهد که مقدار هر ویژگی هرگز نمی‌تواند پس‌از ساخته شدن نمونه‌ای از آن نوع تغییر کند، و همه روش‌ها ازنظر ارجاعی شفاف هستند.
    • مطمئن شوید همه ویژگی‌های کلاس val باشند نه var، و از نوع تغییرناپذیر باشند.
    • انواع اولیه مانند String،‏ Int، و Float همیشه تغییرناپذیر هستند.
    • انواع data class مسطح با ویژگی‌های val اولیه یا String در مدول‌های ساخته‌شده با کامپایلر Compose ازقبل به‌عنوان پایدار استنباط می‌شوند و به‌طور خودکار با .equals() مقایسه می‌شوند—نیازی نیست آن‌ها را با @Immutable حاشیه‌نویسی کنید.
    • اگر تغییرناپذیری ممکن نباشد، باید از حالت Compose برای هر ویژگی تغییرپذیر استفاده کنید.
  • Stable: نوعی را نشان می‌دهد که قابل‌تغییر است. زمان اجرای Compose متوجه نمی‌شود که آیا و چه زمانی هریک از ویژگی‌های عمومی نوع یا رفتار روش نتایج متفاوتی نسبت‌به فراخوانی قبلی ارائه می‌دهد.

مجموعه‌ها و پایداری

یکی از دلایل رایجی که Compose یک کلاس را ناپایدار درنظر می‌گیرد، مجموعه‌ها است. همان‌طور که در صفحه تشخیص مشکلات پایداری ذکر شده است، کامپایلر Compose نمی‌تواند کاملاً مطمئن باشد که میاناهای مجموعه مانند List، Map، و Set واقعاً تغییرناپذیر هستند و بنابراین آن‌ها را به‌عنوان ناپایدار علامت‌گذاری می‌کند.

این کلاس را از راهنمای تشخیص مشکلات پایداری درنظر بگیرید:

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

با فعال بودن حالت پرش قوی (پیش‌فرض در Kotlin 2.0.20 و بالاتر)، مجموعه‌ها و کلاس‌های ناپایدار حاوی مجموعه‌ها ازقبل بازترکیب را بااستفاده از برابری نمونه (===) هرگاه همان مرجع ارسال شود رد می‌کنند.

قبل‌از جایگزین کردن kotlin.collections.List، Set، یا Map با Kotlinx Immutable Collections (ImmutableList، ImmutableSet، ImmutableMap)، یا افزودن قوانین @Immutable، @Stable، یا stability_config.conf به مدل‌های نگهدارنده مجموعه، احتیاط کنید. در حالت پرش قوی، List استاندارد بااستفاده از برابری مرجع (===) پرش می‌کند، درحالی‌که ImmutableList و مدل‌های مجموعه حاشیه‌نویسی‌شده در هر گذر بازترکیب، مقایسه‌های .equals() عنصر به عنصر را اجرا می‌کنند.

برای مجموعه‌های بزرگ یا چیدمان‌های تنبل (LazyColumn،‏ LazyRow،‏ LazyGrid)، بررسی .equals() در هر مورد می‌تواند بیشتر از بازسازی طول بکشد. اگر ویژگی ImmutableList را از data class به ترکیب‌شدنی فرزند منتقل کنید، این هزینه دوبرابر می‌شود:

data class SnackCategory(
    val name: String,
    val snacks: ImmutableList<Snack>
)

@Composable
fun CategorySection(category: SnackCategory) {
    Text(category.name)
    // Compose compares category.snacks twice: once in SnackCategory.equals()
    // for CategorySection, and again for HighlightedSnacks.
    HighlightedSnacks(snacks = category.snacks)
}

به‌جای تحمیل برابری ساختاری بر مجموعه‌ها، مرجع نمونه مجموعه را در سراسر انتشارها حفظ کنید (برای مثال، با ذخیره کردن تبدیل‌ها در ViewModel یا با remember) تا Compose بتواند از استفاده از برابری نمونه (===) صرف‌نظر کند. فقط زمانی‌که نمی‌توانید مرجع نمونه مجموعه را حفظ کنید و محک‌ها تأیید می‌کنند که بررسی‌های برابری ساختاری سریع‌تر از بازترکیب است، از ImmutableList یا حاشیه‌نویسی‌های پایداری در مدل‌های نگهدارنده مجموعه استفاده کنید. اگر از نسخه Kotlin پایین‌تر از ۲.۰.۲۰ استفاده می‌کنید، ابتدا حالت رد شدن قوی را فعال کنید.

حاشیه‌نویسی با Stable یا Immutable

یک راه ممکن برای حل مشکلات پایداری برای کلاس‌های کوچک با بررسی‌های ارزان .equals() این است که کلاس‌های ناپایدار را با @Stable یا @Immutable حاشیه‌نویسی کنید.

حاشیه‌نویسی کلاس، آنچه را که کامپایلر درغیراین‌صورت درباره کلاس شما استنباط می‌کند، لغو می‌کند. این شبیه به !! عملگر در Kotlin است. باید درباره نحوه استفاده از این حاشیه‌نویسی‌ها بسیار مراقب باشید. لغو کردن رفتار کامپایلر می‌تواند منجر به اشکالات پیش‌بینی‌نشده شود، مثلاً composable شما زمانی که انتظار دارید بازترکیب نشود.

وقتی حالت رد شدن قوی فعال است، هنگام تصمیم‌گیری برای افزودن @Stable یا @Immutable، این دستورالعمل‌ها را دنبال کنید:

  • کلاس‌هایی را که قبلاً به‌عنوان پایدار استنباط شده‌اند حاشیه‌نویسی نکنید: کلاس‌هایی با val ویژگی‌های انواع پایدار در ماژول‌های ساخته‌شده با کامپایلر Compose قبلاً به‌عنوان پایدار استنباط شده‌اند.
  • فقط کلاس‌هایی را با بررسی‌های ارزان .equals() حاشیه‌نویسی کنید که نمونه‌ها بازآفرینی می‌شوند: وقتی کامپایلر نمی‌تواند پایداری را استنباط کند (برای مثال، رابط‌ها یا کلاس‌های ماژول‌های غیرCompose) @Stable یا @Immutable را اضافه کنید و یک منبع داده نمونه‌های شیء جدیدی را برای موارد تغییرنکرده در هر انتشار اختصاص می‌دهد، مشروط بر اینکه کلاس کوچک باشد و بررسی .equals() ارزانی داشته باشد.
  • Be cautious with models containing collections: Avoid adding @Stable or @Immutable to data models that hold large collections (List, Set, Map), especially for models used in lazy layouts (LazyColumn, LazyRow, LazyGrid, Pager). Leaving those models unannotated lets Compose compare them using instance equality (===) instead of running structural .equals() checks across every element.

قطعه کد زیر نمونه‌ای حداقلی از کلاس داده مسطح (بدون ویژگی‌های مجموعه) را ارائه می‌دهد که هنگام تعریف در ماژولی که کامپایلر نمی‌تواند پایداری را استنباط کند، به‌عنوان تغییرناپذیر حاشیه‌نویسی شده است:

@Immutable
data class SnackSummary(
    val id: Long,
    val name: String,
    val price: Long
)

چه از حاشیه‌نویسی @Immutable استفاده کنید و چه از @Stable، کامپایلر Compose کلاس SnackSummary را به‌عنوان پایدار علامت‌گذاری می‌کند و آن را بااستفاده از Object.equals() مقایسه می‌کند.

کلاس‌های حاشیه‌نویسی‌شده در مجموعه‌ها

یک composable را درنظر بگیرید که شامل پارامتری از نوع List<Snack> است:

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  unstable snacks: List<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

حتی اگر Snack پایدار باشد یا با @Immutable حاشیه‌نویسی شده باشد، کامپایلر Compose همچنان پارامتر snacks را در HighlightedSnacks به‌عنوان unstable علامت‌گذاری می‌کند زیرا List یک میانای کاربری است.

با فعال بودن حالت رد شدن قوی، HighlightedSnacks همچنان به‌عنوان skippable علامت‌گذاری می‌شود و بااستفاده از برابری نمونه (===) هنگام ارسال همان نمونه List، از بازترکیب رد می‌شود. می‌توانید پارامتر snacks را با قرار دادن List<Snack> در کلاس بسته‌بندی‌کننده @Immutable، جایگزین کردن List<Snack> با ImmutableList<Snack>، یا افزودن kotlin.collections.* به فایل پیکربندی پایداری ، stable کنید.

بااین‌حال، انجام این کار باعث می‌شود Compose در هر گذر بازترکیب، مقایسه عنصر به عنصر .equals() انجام دهد. فقط زمانی از این رویکردها استفاده کنید که نمی‌توانید مرجع نمونه فهرست را حفظ کنید و محک‌ها نشان‌دهنده مزیت باشند.

فایل پیکربندی پایداری

از Compose Compiler 1.5.5 به‌بعد، فایل پیکربندی کلاس‌ها برای درنظر گرفتن پایداری می‌تواند در زمان ترجمه ارائه شود. این کار امکان درنظر گرفتن کلاس‌هایی را که کنترل نمی‌کنید، مثل کلاس‌های کتابخانه استاندارد مانند LocalDateTime، به‌عنوان پایدار فراهم می‌کند.

فایل پیکربندی یک فایل نوشتاری ساده با یک کلاس در هر ردیف است. نظرات، جانشین‌های تکی و دوتایی پشتیبانی می‌شوند.

نمونه پیکربندی:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

برای فعال کردن این ویژگی، مسیر فایل پیکربندی را به بلوک گزینه‌های composeCompiler پیکربندی افزایه Compose compiler Gradle ارسال کنید.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

ازآنجایی‌که کامپایلر Compose به‌طور جداگانه روی هر پیمانه در پروژه شما اجرا می‌شود، می‌توانید درصورت نیاز پیکربندی‌های متفاوتی را برای پیمانه‌های مختلف ارائه دهید. یا اینکه یک پیکربندی در سطح ریشه پروژه‌تان داشته باشید و آن مسیر را به هر واحد ارسال کنید.

چندین پیمانه

مشکل رایج دیگر مربوط به معماری چندماژولی است. کامپایلر Compose تنها درصورتی می‌تواند پایدار بودن کلاس را استنباط کند که همه انواع غیرابتدایی که به آن‌ها ارجاع می‌دهد یا به‌طور صریح به‌عنوان پایدار علامت‌گذاری شده باشند یا در ماژولی باشند که با کامپایلر Compose ساخته شده باشد.

اگر لایه داده شما در واحدی جدا از لایه میانای کاربری شما قرار دارد (که رویکرد توصیه‌شده است)، ممکن است با این مشکل مواجه شوید. در حالت پرش قوی، کلاس‌های ماژول‌های غیرCompose ناپایدار تلقی می‌شوند و همچنان بااستفاده از برابری نمونه (===) هنگام ارسال همان نمونه، از بازترکیب رد می‌شوند. بااین‌حال، اگر لایه داده شما به‌طور مکرر نمونه‌های جدیدی از مدل‌های کوچک و مسطح با مقادیر یکسان در هر انتشار اختصاص می‌دهد، می‌توانید برابری ساختاری (Object.equals()) را انتخاب کنید.

راه‌حل

برای حل این مشکل می‌توانید یکی از رویکردهای زیر را اتخاذ کنید:

  1. کلاس‌های کوچک و مسطح با بررسی‌های .equals() ارزان به فایل پیکربندی کامپایلر خود اضافه کنید.
  2. کامپایلر Compose را در پیمانه‌های لایه داده‌تان فعال کنید، یا کلاس‌هایتان را با @Stable یا @Immutable در جاهای مناسب برچسب‌گذاری کنید.
    • این کار شامل افزودن وابستگی Compose به لایه داده شما است. بااین‌حال، این فقط وابستگی برای زمان اجرای Compose است و نه برای Compose-UI.
  3. در ماژول واسط کاربر، کلاس‌های لایه داده را در کلاس‌های پوشاننده مختص واسط کاربر قرار دهید.

همین مشکل هنگام استفاده از کتابخانه‌های خارجی نیز رخ می‌دهد اگر از کامپایلر Compose استفاده نکنند.

همه composableها نباید قابل‌ردشدن باشند

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

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

  • یک composable که اغلب یا اصلاً بازترکیب نمی‌شود.
  • یک ترکیب‌پذیر که خودش فقط ترکیب‌پذیرهای قابل‌ردشدن را فراخوانی می‌کند.
  • یک composable با تعداد زیادی پارامتر یا مدل‌های مجموعه با پیاده‌سازی‌های گران‌قیمت .equals(). دراین‌صورت، هزینه بررسی اینکه آیا پارامتری تغییر کرده است یا نه می‌تواند از هزینه بازترکیب ارزان بیشتر باشد.

فقط به تعداد بازترکیب‌ها تکیه نکنید. روی ساختن ترکیب‌شدنی‌های سبک برای اجرا تمرکز کنید، خواندن‌های وضعیت به‌سرعت درحال تغییر را به فازهای «چیدمان» یا «ترسیم» موکول کنید، و تغییرات را با محک‌ها تأیید کنید. وقتی یک composable قابل‌ردشدن باشد، سربار کوچکی اضافه می‌کند که ممکن است ارزشش را نداشته باشد. You can even annotate your composable to be non-restartable or non-skippable in cases where you determine that being restartable or skippable is more overhead than it's worth.