مثل معظم حِزم أدوات واجهة المستخدم الأخرى، يعرض Compose إطارًا من خلال عدة مراحل مختلفة. على سبيل المثال، يتضمّن نظام Android View ثلاث مراحل رئيسية: القياس والتخطيط والرسم. يتشابه Compose مع هذه المراحل إلى حد كبير، ولكنه يتضمن مرحلة إضافية مهمة تُسمى الإنشاء في البداية.
توضّح مستندات Compose عملية الإنشاء في مقالتَي التفكير في Compose والحالة وJetpack Compose.
المراحل الثلاث للإطار
يتضمّن Compose ثلاث مراحل رئيسية:
- التركيب: ما هي واجهة المستخدم التي يجب عرضها؟ تُشغِّل Compose الدوال المركّبة وتنشئ وصفًا لواجهة المستخدم.
- التنسيق: مكان وضع واجهة المستخدم تتكون هذه المرحلة من خطوتين: القياس والوضع. تقيس عناصر التنسيق نفسها وأي عناصر فرعية وتضعها في إحداثيات ثنائية الأبعاد، لكل عقدة في شجرة التنسيق.
- الرسم: كيفية عرضه يتم رسم عناصر في واجهة المستخدم في لوحة رسم، وهي عادةً شاشة الجهاز.
يكون ترتيب هذه المراحل هو نفسه بشكل عام، ما يسمح بتدفّق البيانات في اتجاه واحد من التركيب إلى التنسيق إلى الرسم لإنشاء إطار (يُعرف أيضًا باسم تدفّق البيانات أحادي الاتجاه). BoxWithConstraints وLazyColumn وLazyRow هي استثناءات ملحوظة، حيث يعتمد تكوين العناصر الثانوية على مرحلة التنسيق للعنصر الرئيسي.
من الناحية النظرية، تحدث كل مرحلة من هذه المراحل لكل إطار، ولكن لتحسين الأداء، يتجنّب Compose تكرار العمل الذي من شأنه احتساب النتائج نفسها من المدخلات نفسها في جميع هذه المراحل. يتخطّى Compose تشغيل دالة مركّبة إذا كان بإمكانه إعادة استخدام نتيجة سابقة، ولا تعيد واجهة مستخدم Compose تخطيط الشجرة بالكامل أو إعادة رسمها إذا لم يكن ذلك ضروريًا. تؤدي أداة Compose الحد الأدنى فقط من العمل المطلوب لتعديل واجهة المستخدم. هذا التحسين ممكن لأنّ Compose يتتبّع قراءات الحالة ضمن المراحل المختلفة.
فهم المراحل
يوضّح هذا القسم كيفية تنفيذ مراحل Compose الثلاث للدوال المركّبة بمزيد من التفصيل.
مقطوعة موسيقية
في مرحلة التركيب، ينفّذ وقت تشغيل Compose الدوال المركّبة ويعرض بنية شجرية تمثّل واجهة المستخدم. تتكوّن شجرة واجهة المستخدم هذه من عُقد التنسيق التي تحتوي على جميع المعلومات اللازمة للمراحل التالية، كما هو موضّح في الفيديو التالي:
الشكل 2: الشجرة التي تمثّل واجهة المستخدم التي تم إنشاؤها في مرحلة الإنشاء.
تبدو الفقرة الفرعية من الرمز وشجرة واجهة المستخدم على النحو التالي:
في هذه الأمثلة، يتم ربط كل دالة مركّبة في الرمز البرمجي بعقدة تنسيق واحدة في شجرة واجهة المستخدم. في الأمثلة الأكثر تعقيدًا، يمكن أن تحتوي الدوال المركّبة على منطق وتدفق تحكّم، وتنتج شجرة مختلفة في حالات مختلفة.
التنسيق
في مرحلة التنسيق، تستخدم أداة Compose شجرة واجهة المستخدم التي تم إنشاؤها في مرحلة الإنشاء كمدخلات. تحتوي مجموعة عُقد التنسيق على جميع المعلومات اللازمة لتحديد حجم كل عقدة وموقعها في المساحة الثنائية الأبعاد.
الشكل 4. قياس كل عقدة تخطيط وموضعها في شجرة واجهة المستخدم أثناء مرحلة التخطيط
During the layout phase, the tree is traversed using the following three-step algorithm:
- قياس العناصر الفرعية: تقيس العقدة عناصرها الفرعية في حال توفّرها.
- تحديد الحجم الخاص: استنادًا إلى هذه القياسات، يحدّد العقدة حجمه الخاص.
- وضع العناصر الثانوية: يتم وضع كل عنصر ثانوي بالنسبة إلى موضع العنصر نفسه.
في نهاية هذه المرحلة، تحتوي كل عقدة تخطيط على:
- عرض وارتفاع محدَّدان
- إحداثيات x وy التي يجب رسمها
تذكَّر شجرة واجهة المستخدم من القسم السابق:
بالنسبة إلى هذه الشجرة، تعمل الخوارزمية على النحو التالي:
- يقيس العنصر
Rowعناصره الثانوية،ImageوColumn. - يتم قياس
Image. لا يحتوي على أي عناصر فرعية، لذا يحدّد حجمه الخاص ويُبلغ عن الحجم إلىRow. - يتم قياس
Columnبعد ذلك. تقيس الدالة أولاً الدوال الثانوية الخاصة بها (دالتانTextمركّبتان). - يتم قياس أول
Text. لا يحتوي على أي عناصر فرعية، لذا يحدّد حجمه ويُبلغ عن حجمه إلىColumn.- يتم قياس
Textالثاني. لا يحتوي على أي عناصر فرعية، لذا يحدّد حجمه الخاص ويبلغه إلىColumn.
- يتم قياس
- يستخدم
Columnقياسات العناصر الفرعية لتحديد حجمه. ويستخدم الحد الأقصى لعرض العنصر الفرعي ومجموع ارتفاع العناصر الفرعية. - يضع
Columnعناصره الفرعية بالنسبة إلى نفسه، ويضعها تحت بعضها البعض عموديًا. - يستخدم
Rowقياسات العناصر الفرعية لتحديد حجمه. ويستخدم الحد الأقصى لارتفاع العنصر الفرعي ومجموع عروض العناصر الفرعية. ثم يضع عناصره الفرعية.
لاحظ أنه تمت زيارة كل عقدة مرة واحدة فقط. تتطلب بيئة تشغيل Compose تمريرًا واحدًا فقط عبر شجرة واجهة المستخدم لقياس جميع العُقد ووضعها، مما يحسّن الأداء. عندما يزداد عدد العُقد في الشجرة، يزداد الوقت المستغرق في اجتيازها بطريقة خطية. في المقابل، إذا تمت زيارة كل عقدة عدة مرات، سيزيد وقت التنقل بشكل كبير.
الرسم
في مرحلة الرسم، يتم اجتياز الشجرة مرة أخرى من الأعلى إلى الأسفل، وكل عقدة ترسم نفسها على الشاشة بدورها.
الشكل 5: ترسم مرحلة الرسم وحدات البكسل على الشاشة.
باستخدام المثال السابق، يتم رسم محتوى الشجرة بالطريقة التالية:
- يرسم
Rowأي محتوى قد يتضمّنه، مثل لون الخلفية. - يرسم
Imageنفسه. - يرسم
Columnنفسه. - يتم رسم
Textالأول والثاني على التوالي.
الشكل 6: شجرة واجهة مستخدم وتمثيلها المرسوم
قراءات الحالة
عند قراءة value من snapshot state خلال إحدى المراحل
المذكورة سابقًا، يتتبّع Compose تلقائيًا ما كان يفعله عند قراءة
value. يسمح هذا التتبُّع لـ Compose بإعادة تنفيذ القارئ عندما تتغيّر قيمة value للحالة، وهو أساس إمكانية تتبّع البيانات في Compose.
يمكنك عادةً إنشاء الحالة باستخدام mutableStateOf() ثم الوصول إليها من خلال
إحدى طريقتين: إما بالوصول مباشرةً إلى السمة value أو
باستخدام مفوَّض سمة Kotlin. يمكنك الاطّلاع على مزيد من المعلومات عنها في الحالة في الدوال المركّبة. لأغراض هذا الدليل، تشير "قراءة الحالة" إلى أيّ من
طرق الوصول المكافئة هذه.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
في مفوّض السمة، يتم استخدام دالتَي "getter" و"setter" للوصول إلى value الحالة وتعديلها. لا يتم استدعاء دالتي getter وsetter إلا عند الإشارة إلى السمة كقيمة، وليس عند إنشائها، ولهذا السبب فإن الطريقتين الموصوفتين سابقًا متكافئتان.
كل مجموعة من الرموز البرمجية التي يمكن إعادة تنفيذها عند تغيير حالة القراءة هي
نطاق إعادة تشغيل. يتتبّع Compose تغييرات الحالة value ويعيد تشغيل النطاقات
في مراحل مختلفة.
قراءات الحالة على مراحل
كما ذكرنا سابقًا، هناك ثلاث مراحل رئيسية في Compose، ويتتبّع Compose الحالة التي تتم قراءتها في كل منها. يتيح ذلك لـ Compose إرسال إشعارات فقط إلى المراحل المحدّدة التي تحتاج إلى تنفيذ العمل لكل عنصر متأثر في واجهة المستخدم.
توضّح الأقسام التالية كل مرحلة وما يحدث عند قراءة قيمة الحالة فيها.
المرحلة 1: التركيبة
تؤثر عمليات قراءة الحالة ضمن دالة @Composable أو كتلة lambda في عملية الإنشاء
وربما في المراحل اللاحقة. عندما يتغيّر value الحالة، يجدول المُعيد التركيب عمليات إعادة تشغيل لجميع الدوال المركّبة التي تقرأ value الحالة. يُرجى العِلم أنّ وقت التشغيل قد يتخطّى بعض الدوال المركّبة أو جميعها إذا لم تتغيّر المدخلات. لمزيد من المعلومات، يُرجى الاطّلاع على التخطّي في حال عدم تغيير المدخلات.
بناءً على نتيجة الإنشاء، تنفّذ واجهة مستخدم Compose مرحلتَي التخطيط والرسم. وقد يتخطّى هذه المراحل إذا ظلّ المحتوى كما هو ولم يتغيّر الحجم والتنسيق.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
المرحلة 2: التخطيط
تتألف مرحلة التنسيق من خطوتَين: القياس والموضع. تنفّذ خطوة القياس دالة lambda للقياس التي تم تمريرها إلى الدالة المركّبة Layout، وطريقة MeasureScope.measure لواجهة LayoutModifier، من بين طرق أخرى.
تنفّذ خطوة الموضع مجموعة مواضع دالة layout ومجموعة lambda لدالة Modifier.offset { … } والدوال المشابهة.
تؤثر قراءات الحالة خلال كل خطوة من هذه الخطوات في التنسيق وربما في مرحلة الرسم. عندما تتغيّر قيمة value في الحالة، يجدول Compose UI مرحلة التنسيق. ويشغّل أيضًا مرحلة الرسم في حال تغيّر الحجم أو الموضع.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
المرحلة 3: الرسم
تؤثر قراءات الحالة أثناء رسم الرمز البرمجي في مرحلة الرسم. وتشمل الأمثلة الشائعة Canvas() وModifier.drawBehind وModifier.drawWithContent. عندما تتغيّر حالة value، تنفّذ واجهة مستخدم Compose مرحلة الرسم فقط.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
تحسين عمليات قراءة الحالة
بما أنّ Compose يتتبّع قراءة الحالة المترجَمة، يمكنك تقليل مقدار العمل الذي يتم تنفيذه من خلال قراءة كل حالة في مرحلة مناسبة.
انظر المثال التالي. يحتوي هذا المثال على Image() يستخدم
معدِّل الإزاحة لإزاحة موضع التنسيق النهائي، ما يؤدي إلى تأثير
اختلاف المنظر أثناء تمرير المستخدم.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
هذا الرمز يعمل، ولكنّه يؤدي إلى أداء غير مثالي. كما هو مكتوب، يقرأ الرمز value لحالة firstVisibleItemScrollOffset ويمرّره إلى دالة Modifier.offset(offset: Dp). أثناء تنقّل المستخدم في الصفحة، سيتغيّر value في "firstVisibleItemScrollOffset". كما تعلّمت، يتتبّع Compose أي قراءات للحالة ليتمكّن من إعادة تشغيل (إعادة استدعاء) رمز القراءة، وهو في هذا المثال محتوى Box.
هذا مثال على قراءة حالة ضمن مرحلة الإنشاء. هذا ليس بالضرورة أمرًا سيئًا، بل هو في الواقع أساس إعادة التركيب، مما يسمح لتغييرات البيانات بإصدار واجهة مستخدم جديدة.
نقطة أساسية: هذا المثال ليس مثاليًا لأنّ كل حدث تمرير يؤدي إلى إعادة تقييم المحتوى المركّب بالكامل وقياسه وتخطيطه ورسمه في النهاية. يتم تفعيل مرحلة الإنشاء عند كل تمرير على الرغم من أنّ المحتوى المعروض لم يتغيّر، بل موضعه فقط. يمكنك تحسين قراءة الحالة لإعادة تشغيل مرحلة التنسيق فقط.
الإزاحة باستخدام lambda
يتوفّر إصدار آخر من معدِّل الإزاحة:
Modifier.offset(offset: Density.() -> IntOffset).
يستخدم هذا الإصدار مَعلمة lambda، حيث يتم عرض الإزاحة الناتجة من خلال كتلة lambda. يُرجى تعديل الرمز لاستخدامه:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
إذًا، لماذا يُعدّ هذا الخيار أفضل من حيث الأداء؟ يتم استدعاء كتلة lambda التي تقدّمها للمعدِّل أثناء مرحلة التنسيق (على وجه التحديد، أثناء خطوة تحديد الموضع في مرحلة التنسيق)، ما يعني أنّه لم يعُد تتم قراءة حالة firstVisibleItemScrollOffset أثناء الإنشاء. بما أنّ Compose يتتبّع وقت قراءة الحالة،
يعني هذا التغيير أنّه في حال تغيّر value في firstVisibleItemScrollOffset،
على Compose إعادة تشغيل مرحلتَي التنسيق والرسم فقط.
Of course, it is often absolutely necessary to read states in the composition
phase. ومع ذلك، هناك حالات يمكنك فيها تقليل عدد عمليات إعادة الإنشاء من خلال فلترة تغييرات الحالة. لمزيد من المعلومات حول هذا الموضوع،
يُرجى الاطّلاع على derivedStateOf: تحويل عنصر حالة واحد أو أكثر إلى حالة أخرى.
حلقة إعادة التركيب (تبعية المرحلة الدورية)
ذكرنا سابقًا في هذا الدليل أنّ مراحل Compose يتم استدعاؤها دائمًا بالترتيب نفسه، ولا يمكن الرجوع إلى مرحلة سابقة أثناء التواجد في الإطار نفسه. ومع ذلك، لا يمنع ذلك التطبيقات من الدخول في حلقات تركيبية عبر إطارات مختلفة. على سبيل المثال:
Box { var imageHeightPx by remember { mutableIntStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
ينفّذ هذا المثال عمودًا رأسيًا، مع الصورة في الأعلى، ثم النص أسفلها. يستخدم Modifier.onSizeChanged() للحصول على الحجم الذي تم حله
للصورة، ثم يستخدم Modifier.padding() على النص لتحريكه لأسفل.
يشير التحويل غير الطبيعي من Px إلى Dp إلى أنّ الرمز البرمجي يتضمّن مشكلة.
المشكلة في هذا المثال هي أنّ الرمز لا يصل إلى التنسيق "النهائي" ضمن إطار واحد. يعتمد الرمز على حدوث إطارات متعددة، ما يؤدي إلى تنفيذ عمل غير ضروري، ويؤدي إلى انتقال واجهة المستخدم على الشاشة للمستخدم.
إنشاء الإطار الأول
أثناء مرحلة إنشاء الإطار الأول، تكون قيمة imageHeightPx في البداية
0. وبالتالي، يوفّر الرمز النص مع Modifier.padding(top = 0).
تستدعي مرحلة التنسيق اللاحقة دالة الاستدعاء الخاصة بالمعدِّل onSizeChanged،
والتي تعدّل imageHeightPx إلى الارتفاع الفعلي للصورة. بعد ذلك، يجدول Compose عملية إعادة تركيب للإطار التالي. ومع ذلك، أثناء مرحلة الرسم الحالية، يتم عرض النص مع مساحة متروكة تبلغ 0، لأنّ قيمة imageHeightPx المعدَّلة لم تظهر بعد.
تركيب الإطار الثاني
تبدأ Compose الإطار الثاني، والذي يتم تشغيله من خلال التغيير في قيمة imageHeightPx. في مرحلة إنشاء هذا الإطار، تتم قراءة الحالة ضمن مجموعة محتوى Box. يتم الآن توفير النص مع مساحة متروكة تتطابق بدقة مع ارتفاع الصورة. أثناء مرحلة التنسيق، يتم ضبط imageHeightPx مرة أخرى، ولكن
لا تتم جدولة أي إعادة تركيب أخرى لأنّ القيمة تظل متسقة.
قد يبدو هذا المثال مصطنعًا، ولكن احذر من هذا النمط العام:
Modifier.onSizeChanged()أوonGloballyPositioned()أو بعض عمليات التنسيق الأخرى- تعديل بعض الحالات
- استخدِم هذه الحالة كإدخال لمعدِّل تنسيق (
padding()أوheight()أو ما شابه) - يُحتمَل أن تكون مكرّرة
لإصلاح النموذج السابق، يجب استخدام العناصر الأساسية المناسبة للتنسيق. يمكن تنفيذ المثال السابق باستخدام Column()، ولكن قد يكون لديك مثال أكثر تعقيدًا يتطلب شيئًا مخصصًا، مما يتطلب كتابة تخطيط مخصص. لمزيد من المعلومات، يُرجى الاطّلاع على دليل التنسيقات المخصّصة.
المبدأ العام هنا هو أن يكون لديك مصدر واحد للحقيقة لعناصر واجهة المستخدم المتعددة التي يجب قياسها ووضعها فيما يتعلق ببعضها البعض. إنّ استخدام عنصر أساسي مناسب للتنسيق أو إنشاء تنسيق مخصّص يعني أنّ العنصر الرئيسي المشترك الأدنى هو مصدر المعلومات الذي يمكنه تنسيق العلاقة بين عناصر متعددة. ويؤدي تقديم حالة ديناميكية إلى مخالفة هذا المبدأ.
لمزيد من المعلومات حول تكرار إعادة الإنشاء وكيفية تجنُّب الكتابة في الحالة على مستوى المراحل، يُرجى الاطّلاع على الكتابة العكسية في Compose.
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- State وJetpack Compose
- القوائم والشبكات
- Kotlin لـ Jetpack Compose