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

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

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

ابتدا باید سعی کنید حالت پرش قوی (strong skipping mode) را فعال کنید. حالت پرش قوی (strong skipping mode) اجازه می‌دهد تا از ترکیب‌پذیرهایی (composables) که پارامترهای ناپایدار دارند، صرف‌نظر شود و ساده‌ترین روش برای رفع مشکلات عملکردی ناشی از پایداری است.

برای اطلاعات بیشتر به پرش قوی مراجعه کنید.

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

همچنین می‌توانید سعی کنید یک کلاس ناپایدار را کاملاً تغییرناپذیر کنید.

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

مجموعه‌های تغییرناپذیر

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

برای حل این مشکل، می‌توانید از مجموعه‌های تغییرناپذیر استفاده کنید. کامپایلر Compose از مجموعه‌های تغییرناپذیر Kotlinx پشتیبانی می‌کند. این مجموعه‌ها تضمین شده‌اند که تغییرناپذیر باشند و کامپایلر Compose با آنها به همین صورت رفتار می‌کند. این کتابخانه هنوز در مرحله آلفا است، بنابراین انتظار تغییرات احتمالی در API آن را داشته باشید.

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

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

شما می‌توانید با استفاده از یک مجموعه تغییرناپذیر، tags پایدار کنید. در کلاس، نوع tags را به ImmutableSet<String> تغییر دهید:

data class Snack{
    
    val tags: ImmutableSet<String> = persistentSetOf()
    
}

پس از انجام این کار، تمام پارامترهای کلاس تغییرناپذیر هستند و کامپایلر Compose کلاس را به عنوان پایدار علامت‌گذاری می‌کند.

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

یک راه ممکن برای حل مشکلات پایداری، حاشیه‌نویسی کلاس‌های ناپایدار با @Stable یا @Immutable است.

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

اگر می‌توانید کلاس خود را بدون حاشیه‌نویسی پایدار کنید، باید از این طریق برای دستیابی به ثبات تلاش کنید.

قطعه کد زیر یک مثال مینیمال از یک کلاس داده با حاشیه‌نویسی تغییرناپذیر (immutable) ارائه می‌دهد:

@Immutable
data class Snack(

)

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

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

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

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  
  unstable snacks: List<Snack>
  
)

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

پارامترها در مورد انواع مجموعه با همان مشکل کلاس‌ها مواجه می‌شوند، کامپایلر Compose همیشه پارامتری از نوع List را به عنوان ناپایدار علامت‌گذاری می‌کند ، حتی زمانی که مجموعه‌ای از انواع پایدار باشد.

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

روش‌های مختلفی برای حل مشکل مجموعه‌های ناپایدار وجود دارد. بخش‌های بعدی این رویکردهای مختلف را شرح می‌دهند.

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

اگر از پایبندی به قرارداد پایداری در کدبیس خود راضی هستید، می‌توانید با اضافه کردن kotlin.collections.* به فایل پیکربندی پایداری خود، مجموعه‌های کاتلین را پایدار در نظر بگیرید.

مجموعه تغییرناپذیر

برای امنیت در زمان کامپایل و جلوگیری از تغییرناپذیری، می‌توانید به جای List از یک مجموعه تغییرناپذیر kotlinx استفاده کنید.

@Composable
private fun HighlightedSnacks(
    
    snacks: ImmutableList<Snack>,
    
)

بسته بندی

اگر نمی‌توانید از یک مجموعه تغییرناپذیر استفاده کنید، می‌توانید مجموعه خودتان را بسازید. برای انجام این کار، List در یک کلاس پایدار حاشیه‌نویسی شده قرار دهید. بسته به نیاز شما، یک پوشش عمومی احتمالاً بهترین انتخاب برای این کار است.

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

سپس می‌توانید از این به عنوان نوع پارامتر در composable خود استفاده کنید.

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

راه حل

پس از اتخاذ هر یک از این رویکردها، کامپایلر Compose اکنون HighlightedSnacks Composable را هم به عنوان skippable و هم restartable علامت‌گذاری می‌کند.

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

در طول ترکیب مجدد، Compose اکنون می‌تواند HighlightedSnacks نادیده بگیرد اگر هیچ یک از ورودی‌های آن تغییر نکرده باشد.

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

با شروع 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 از پیکربندی افزونه Gradle کامپایلر Compose ارسال کنید.

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

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

ماژول‌های چندگانه

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

اگر لایه داده شما در یک ماژول جداگانه از لایه رابط کاربری شما قرار دارد، که رویکرد پیشنهادی است، ممکن است با این مشکل مواجه شوید.

راه حل

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

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

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

هر ترکیب‌بندی‌ای نباید قابل رد شدن باشد

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

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

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

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