إصلاح مشاكل الثبات

عند مواجهة فئة غير مستقرة تسبّب مشاكل في الأداء، عليك تحسين طريقة مقارنة Compose لها أو إنشاء مثيل لها. يوضّح هذا المستند عدّة أساليب يمكنك استخدامها لإجراء ذلك.

تفعيل ميزة "التخطّي القوي"

يتم تفعيل وضع التخطّي القوي تلقائيًا بدءًا من الإصدار 2.0.20 من Kotlin. يتيح وضع التخطّي القوي تخطّي الدوال المركّبة التي تتضمّن مَعلمات غير ثابتة من خلال مقارنة المَعلمات غير الثابتة باستخدام مساواة المثيلات (===)، بينما يستمر مقارنة المَعلمات الثابتة باستخدام المساواة الهيكلية (Object.equals()).

إذا كنت تستخدم إصدار Kotlin أقدم من 2.0.20، فعليك تفعيل وضع التخطّي القوي أولاً. إذا كان وضع التخطّي القوي مفعَّلاً ولا تزال الدالة المركّبة تُجري إعادة تركيب غير ضرورية، تحقَّق مما إذا كان يتم تخصيص نُسخ جديدة من المَعلمات غير الثابتة في كل عملية إعادة تركيب.

لمزيد من المعلومات، يُرجى الاطّلاع على التخطّي القوي.

جعل الفئة غير قابلة للتغيير

يمكنك أيضًا جعل فئة غير مستقرة غير قابلة للتغيير تمامًا حتى يتمكّن برنامج التجميع من استنتاج ثباتها تلقائيًا.

  • غير قابل للتغيير: يشير إلى نوع لا يمكن فيه تغيير قيمة أي سمات بعد إنشاء مثيل من هذا النوع، وتكون جميع الطرق شفافة من الناحية المرجعية.
    • تأكَّد من أنّ جميع سمات الفئة هي val بدلاً من var، وأنّها من أنواع غير قابلة للتغيير.
    • الأنواع الأساسية مثل String وInt وFloat تكون دائمًا غير قابلة للتغيير.
    • إنّ أنواع data class الثابتة التي تتضمّن سمات val أساسية أو String في الوحدات التي تم إنشاؤها باستخدام برنامج تجميع Compose يتم استنتاجها تلقائيًا على أنّها ثابتة وتتم مقارنتها بـ .equals() تلقائيًا، لذا لا حاجة إلى إضافة تعليقات توضيحية إليها باستخدام @Immutable.
    • إذا كانت الثبات مستحيلاً، فيجب عليك استخدام حالة الإنشاء لأي خصائص قابلة للتغيير.
  • ثابت: يشير إلى نوع قابل للتغيير. لا يدرك وقت تشغيل Compose ما إذا كانت أي من الخصائص العامة للنوع أو سلوك الطريقة ستؤدي إلى نتائج مختلفة عن الاستدعاء السابق ومتى يحدث ذلك.

المجموعات والثبات

من الأسباب الشائعة التي تجعل Compose يعتبر فئة غير مستقرة هي المجموعات. كما هو موضّح في صفحة تشخيص مشاكل الثبات، لا يمكن لبرنامج تجميع Compose التأكّد تمامًا من أنّ واجهات المجموعات، مثل List وMap وSet، غير قابلة للتغيير حقًا، وبالتالي يضع علامة عليها على أنّها غير ثابتة.

لنفترض أنّ لدينا هذه الفئة من دليل تشخيص مشاكل الثبات:

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

عند تفعيل وضع التخطّي القوي (الوضع التلقائي في الإصدار 2.0.20 من Kotlin والإصدارات الأحدث)، تتخطّى المجموعات والفئات غير الثابتة التي تحتوي على مجموعات إعادة الإنشاء باستخدام مساواة المثيلات (===) كلما تم تمرير المرجع نفسه.

يُرجى توخي الحذر قبل استبدال kotlin.collections.List أو Set أو Map بمجموعات Kotlinx غير القابلة للتغيير (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 أقدم من 2.0.20، فعِّل وضع التخطّي القوي أولاً.

إضافة تعليقات توضيحية باستخدام Stable أو Immutable

من الطرق الممكنة لحلّ مشاكل الثبات في الفئات الصغيرة التي تتضمّن عمليات تحقّق .equals() غير مكلفة هي إضافة تعليقات توضيحية إلى الفئات غير الثابتة باستخدام @Stable أو @Immutable.

إنّ إضافة تعليقات توضيحية إلى فئة يؤدي إلى إلغاء ما قد يستنتجه برنامج التجميع بشأن الفئة. وهو مشابه لعامل التشغيل !! في Kotlin. يجب أن تكون حذرًا للغاية بشأن كيفية استخدام هذه التعليقات التوضيحية. قد يؤدي تجاوز سلوك برنامج التجميع إلى حدوث أخطاء غير متوقعة، مثل عدم إعادة تركيب الدالة المركّبة عندما تتوقع ذلك.

عند تفعيل وضع التخطّي القوي، اتّبِع هذه الإرشادات عند تحديد ما إذا كنت تريد إضافة @Stable أو @Immutable:

  • لا تضِف تعليقات توضيحية إلى الفئات التي تم استنتاجها على أنّها ثابتة: الفئات التي تحتوي على سمات val من الأنواع الثابتة في الوحدات التي تم إنشاؤها باستخدام برنامج تجميع Compose تم استنتاجها على أنّها ثابتة.
  • إضافة تعليقات توضيحية إلى الفئات التي تتضمّن عمليات تحقّق .equals() منخفضة التكلفة فقط عند إعادة إنشاء المثيلات: يمكنك إضافة @Stable أو @Immutable عندما يتعذّر على برنامج التجميع استنتاج الثبات (على سبيل المثال، الواجهات أو الفئات من وحدات غير Compose) ويخصّص مصدر البيانات مثيلات كائنات جديدة للعناصر التي لم تتغيّر في كل عملية إرسال، بشرط أن تكون الفئة صغيرة وتتضمّن عملية تحقّق .equals() منخفضة التكلفة.
  • يُرجى توخي الحذر عند استخدام النماذج التي تحتوي على مجموعات: تجنَّب إضافة @Stable أو @Immutable إلى نماذج البيانات التي تحتوي على مجموعات كبيرة (List أو Set أو Map)، خاصةً النماذج المستخدَمة في التنسيقات الكسولة (LazyColumn أو LazyRow أو LazyGrid أو Pager). يتيح ترك هذه النماذج بدون تعليقات توضيحية لأداة Compose مقارنتها باستخدام مساواة المثيلات (===) بدلاً من إجراء عمليات التحقّق .equals() من البنية في كل عنصر.

يقدّم المقتطف التالي مثالاً بسيطًا لفئة بيانات مسطّحة (بدون خصائص المجموعة) تمّت إضافة تعليقات توضيحية إليها على أنّها غير قابلة للتغيير عند تعريفها في وحدة لا يمكن لبرنامج التجميع فيها استنتاج الثبات:

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

سواء كنت تستخدم التعليق التوضيحي @Immutable أو @Stable، فإنّ برنامج تجميع Compose يصنّف الفئة SnackSummary على أنّها ثابتة ويقارنها باستخدام Object.equals().

الصفوف التي تتضمّن تعليقات توضيحية في المجموعات

لنفترض أنّ لدينا دالة مركّبة تتضمّن مَعلمة من النوع 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 stable من خلال تضمين List<Snack> في فئة تضمين @Immutable، أو استبدال List<Snack> بـ ImmutableList<Snack>، أو إضافة kotlin.collections.* إلى ملف إعداد الثبات.

ومع ذلك، يؤدي ذلك إلى تشغيل Compose لمقارنة .equals() عنصر بعنصر في كل عملية إعادة تركيب. لا تستخدِم هذه الطرق إلا عندما لا تتمكّن من الاحتفاظ بمرجع مثيل القائمة وتُظهر مقاييس الأداء فائدة من ذلك.

ملف إعداد الثبات

بدءًا من الإصدار 1.5.5 من برنامج تجميع Compose، يمكن توفير ملف إعداد للفئات التي تُعتبر ثابتة في وقت التجميع. يتيح ذلك اعتبار الفئات التي لا تتحكّم فيها، مثل فئات المكتبة العادية مثل 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.

إذا كانت طبقة البيانات في وحدة منفصلة عن طبقة واجهة المستخدم، وهو النهج الموصى به، فقد تكون هذه مشكلة تواجهها. في وضع التخطّي القوي، يتم التعامل مع الفئات من الوحدات غير المستندة إلى Compose على أنّها غير مستقرة ولا تزال تتخطّى إعادة الإنشاء باستخدام مساواة المثيلات (===) عند تمرير المثيل نفسه. ومع ذلك، إذا كانت طبقة البيانات تخصص بشكل متكرر مثيلات جديدة من النماذج الصغيرة المسطحة ذات القيم المتطابقة في كل انبعاث، يمكنك اختيار المساواة الهيكلية (Object.equals()).

الحل

لحلّ هذه المشكلة، يمكنك اتّباع أحد الأساليب التالية:

  1. أضِف فئات صغيرة ومسطّحة مع عمليات تحقّق من .equals() منخفضة التكلفة إلى ملف إعداد برنامج التجميع.
  2. فعِّل برنامج تجميع Compose في وحدات طبقة البيانات، أو ضَع علامة على فئاتك باستخدام @Stable أو @Immutable حيثما كان ذلك مناسبًا.
    • يتضمن ذلك إضافة تبعية Compose إلى طبقة البيانات. ومع ذلك، فهي مجرد تبعية لوقت تشغيل Compose وليست لـ Compose-UI.
  3. ضمن وحدة واجهة المستخدم، يمكنك تضمين فئات طبقة البيانات في فئات تضمين خاصة بواجهة المستخدم.

تحدث المشكلة نفسها أيضًا عند استخدام مكتبات خارجية إذا لم تكن تستخدم برنامج تجميع Compose.

لا يجب أن تكون كل دالة مركّبة قابلة للتخطّي

عند العمل على حلّ المشاكل المتعلّقة بالثبات، يجب عدم محاولة جعل كل دالة مركّبة قابلة للتخطّي أو فرض المساواة الهيكلية على كل مَعلمة. إنّ تخطّي دالة مركّبة ليس مجانيًا، لأنّ كل مَعلمة في دالة مركّبة قابلة للتخطّي يتم تخزينها في وقت تشغيل Compose كما لو تم تذكّرها، ويجب أن تقارن Compose كل مَعلمة بقيمتها السابقة في كل مرة تتم فيها إعادة تركيب الدالة لتحديد ما إذا كان يجب تخطّيها. قد تؤدي محاولة فرض التخطّي في كل مكان إلى تحسين سابق لأوانه، ما يؤدي إلى زيادة النفقات العامة أكثر من توفيرها.

هناك العديد من الحالات التي لا يكون فيها فرض الاستقرار الهيكلي مفيدًا، بل قد يؤدي إلى تدهور الأداء أو صعوبة الصيانة. على سبيل المثال:

  • دالة مركّبة لا تتم إعادة تركيبها كثيرًا أو على الإطلاق
  • دالة مركّبة تستدعي في حد ذاتها دوال مركّبة قابلة للتخطّي فقط
  • دالة مركّبة تحتوي على عدد كبير من المَعلمات أو نماذج المجموعات مع عمليات تنفيذ .equals() مكلفة. في هذه الحالة، قد تكون تكلفة التحقّق من تغيير أي مَعلمة أكبر من تكلفة إعادة تركيب بسيطة.

لا تفرط في التركيز على أعداد إعادة التركيب وحدها. يجب التركيز على جعل الدوال المركّبة خفيفة الوزن لتنفيذها، وتأجيل قراءات الحالة المتغيّرة بسرعة إلى مرحلتَي التنسيق أو الرسم، والتحقّق من التغييرات باستخدام مقاييس الأداء. عندما تكون الدالة المركّبة قابلة للتخطّي، فإنّها تضيف عبئًا صغيرًا قد لا يستحق ذلك. يمكنك أيضًا إضافة تعليقات توضيحية إلى الدالة المركّبة لتكون غير قابلة لإعادة التشغيل أو غير قابلة للتخطّي في الحالات التي تحدّد فيها أنّ إمكانية إعادة التشغيل أو التخطّي تزيد من عبء العمل أكثر من قيمتها.