مراحل نشاط التركيبات

في هذه الصفحة، ستتعرّف على مراحل نشاط الدالة المركّبة وكيفية تحديد Compose ما إذا كانت الدالة المركّبة بحاجة إلى إعادة تركيب.

نظرة عامة على مراحل النشاط

كما هو موضّح في مستند إدارة الحالة، يصف التركيب واجهة المستخدم في تطبيقك ويتم إنشاؤه من خلال تشغيل الدوال المركّبة. "التركيب" هو بنية شجرية للدوال المركّبة التي تصف واجهة المستخدم.

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

لا يمكن إنشاء "تركيبة" إلا من خلال تركيبة أولية، ولا يمكن تعديلها إلا من خلال إعادة التركيب. الطريقة الوحيدة لتعديل "إنشاء" هي من خلال إعادة الإنشاء.

مخطّط بياني يعرض مراحل النشاط للدالة المركّبة
الشكل 1: مراحل الدالة المركّبة في التركيب ويدخل إلى Composition، وتتم إعادة تركيبه لعدد مرات يساوي صفرًا أو أكثر، ثم يغادر Composition.

يتم عادةً بدء إعادة الإنشاء من خلال تغيير في عنصر State<T>. يتتبّع Compose هذه التغييرات ويشغّل جميع الدوال المركّبة في التركيب التي تقرأ State<T> المحدّد، وأي دوال مركّبة تستدعيها ولا يمكن تخطّيها.

إذا تم استدعاء دالة مركّبة عدة مرات، يتم وضع نُسخ متعددة في التركيب. لكل مكالمة مراحل نشاط خاصة بها في Composition.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

مخطّط بياني يُظهر الترتيب الهرمي للعناصر في مقتطف الرمز السابق
الشكل 2. تمثيل "MyComposable" في "المقطوعة الموسيقية" إذا تم استدعاء دالة مركّبة عدة مرات، يتم وضع نُسخ متعددة في التركيب. ويشير اختلاف لون العنصر إلى أنّه نسخة منفصلة.

بنية الدالة المركّبة في Composition

يتم تحديد مثيل دالة مركّبة في "التركيب" من خلال موقع الاستدعاء. يعتبر برنامج تجميع Compose كل موقع استدعاء مختلفًا. سيؤدي استدعاء الدوال المركّبة من عدة مواقع استدعاء إلى إنشاء نُسخ متعددة من الدالة المركّبة في "التركيب".

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

يُعدّ الحفاظ على الهوية أمرًا بالغ الأهمية لربط الآثار الجانبية بالدالة المركّبة، حتى تكتمل بنجاح بدلاً من إعادة تشغيلها لكل إعادة تكوين.

انظر المثال التالي:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

في مقتطف الرمز أعلاه، سيستدعي LoginScreen الدالة المركّبة LoginError بشكل مشروط، وسيستدعي دائمًا الدالة المركّبة LoginInput. لكل استدعاء موقع استدعاء وموضع مصدر فريد، وسيستخدمهما برنامج التجميع لتحديده بشكل فريد.

مخطّط بياني يوضّح كيفية إعادة إنشاء الرمز السابق في حال تغيير علامة showError إلى &quot;صحيح&quot; تتم إضافة الدالة المركّبة LoginError، ولكن لا تتم إعادة تركيب الدوال المركّبة الأخرى.
الشكل 3: تمثيل LoginScreen في التركيبة عند تغيُّر الحالة وحدوث إعادة تركيب ويعني اللون نفسه أنّه لم تتم إعادة إنشائه.

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

إضافة معلومات إضافية للمساعدة في عمليات إعادة التركيب الذكية

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

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

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

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

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

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

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

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

من خلال تضمين مجموعة من الرموز البرمجية مع استدعاء الدالة المركّبة "key" مع تمرير قيمة واحدة أو أكثر، سيتم دمج هذه القيم لاستخدامها في تحديد هذا المثيل في التركيب. لا يجب أن تكون قيمة key فريدة على مستوى العالم، بل يجب أن تكون فريدة فقط بين استدعاءات الدوال المركّبة في موقع الاستدعاء. في هذا المثال، يجب أن يكون لكل movie key فريد من نوعه بين movies، ولا بأس إذا كان يشارك هذا key مع دالة مركّبة أخرى في مكان آخر في التطبيق.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

باستخدام ما سبق، حتى إذا تغيّرت العناصر في القائمة، يتعرّف Compose على الطلبات الفردية إلى MovieOverview ويمكنه إعادة استخدامها.

مخطّط بياني يوضّح كيفية إعادة إنشاء الرمز البرمجي السابق في حال إضافة عنصر جديد إلى أعلى القائمة بما أنّ عناصر القائمة يتم تحديدها باستخدام مفاتيح، يعرف Compose أنّه لا يجب إعادة إنشائها، على الرغم من تغيُّر مواضعها.
الشكل 6: تمثيل MoviesScreen في Composition عند إضافة عنصر جديد إلى القائمة بما أنّ الدوال المركّبة MovieOverview لها مفاتيح فريدة، يتعرّف Compose على نُسخ MovieOverview التي لم تتغيّر، ويمكنه إعادة استخدامها، وستستمر آثارها الجانبية في التنفيذ.

تتضمّن بعض الدوال المركّبة دعمًا مضمّنًا للدالة المركّبة key. على سبيل المثال، يقبل LazyColumn تحديد key مخصّص في لغة DSL لـ items.

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

التخطّي في حال عدم تغيير المدخلات

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

تكون الدالة المركّبة مؤهَّلة للتخطّي ما لم:

  • تحتوي الدالة على نوع قيمة تم إرجاعها غير Unit
  • تمت إضافة تعليق توضيحي إلى الدالة باستخدام @NonRestartableComposable أو @NonSkippableComposable
  • هناك مَعلمة مطلوبة من نوع غير ثابت

هناك وضع في برنامج التجميع يُعرف باسم التخطّي القوي، وهو يخفّف من الشرط الأخير.

لكي يُعتبر النوع مستقرًا، يجب أن يتوافق مع العقد التالي:

  • ستكون نتيجة equals لمثيلين دائمًا هي نفسها للمثيلين نفسهما.
  • إذا تغيّرت سمة علنية من النوع، سيتم إشعار Composition.
  • جميع أنواع المواقع العلنية مستقرة أيضًا.

هناك بعض الأنواع الشائعة المهمة التي تندرج ضمن هذا العقد والتي سيعتبرها برنامج تجميع Compose ثابتة، على الرغم من عدم وضع علامة عليها صراحةً على أنّها ثابتة باستخدام التعليق التوضيحي @Stable:

  • جميع أنواع القيم الأساسية: Boolean وInt وLong وFloat وChar وما إلى ذلك
  • سلاسل نصية
  • جميع أنواع الدوال (lambdas)

يمكن لجميع هذه الأنواع اتّباع عقد الثبات لأنّها غير قابلة للتغيير. بما أنّ الأنواع غير القابلة للتغيير لا تتغيّر أبدًا، لن تحتاج أبدًا إلى إرسال إشعار إلى Composition بشأن التغيير، لذا من الأسهل بكثير اتّباع هذا العقد.

من الأنواع البارزة الثابتة ولكن يمكن تغييرها هو نوع MutableState في Compose. إذا تم الاحتفاظ بقيمة في MutableState، يُعتبر كائن الحالة بشكل عام مستقرًا لأنّ Compose سيتم إعلامه بأي تغييرات في السمة .value لـ State.

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

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

إذا لم يتمكّن Compose من استنتاج أنّ النوع ثابت، ولكنك تريد فرض اعتباره ثابتًا، يمكنك وضع علامة عليه باستخدام التعليق التوضيحي @Stable.

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

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