مثل معظم أدوات واجهة المستخدم الأخرى، يعرض Compose إطارًا من خلال عدة مراحل مميزة. على سبيل المثال، يتضمّن نظام العرض في Android ثلاث مراحل رئيسية: القياس والتصميم والرسم. تشبه Compose إلى حد كبير، ولكنّها تتضمّن مرحلة إضافية مهمة تُعرف باسم التركيب في البداية.
تصف مستندات Compose عملية الإنشاء في التفكير في Compose والحالة وJetpack Compose.
المراحل الثلاث للإطار
تتضمّن ميزة "إنشاء الأغاني" ثلاث مراحل رئيسية:
- التركيب: واجهة المستخدم التي سيتم عرضها تنفِّذ Compose الدوال البرمجية القابلة للإنشاء وتنشئ وصفًا لواجهة المستخدم.
- التنسيق: مكان وضع واجهة المستخدم تتألف هذه المرحلة من خطوتَين: القياس والموضع. تقيس عناصر التنسيق وتضع نفسها وأي عناصر ثانوية في إحداثيات ثنائية الأبعاد، وذلك لكل عقدة في شجرة التنسيق.
- الرسم: كيفية العرض يتم رسم عناصر واجهة المستخدم في لوحة رسم، وهي عادةً شاشة جهاز.
ويكون ترتيب هذه المراحل هو نفسه بشكل عام، ما يسمح بتدفّق البيانات في اتجاه واحد من الإنشاء إلى التنسيق إلى الرسم لإنتاج إطار (يُعرف أيضًا باسم تدفّق البيانات أحادي الاتجاه). BoxWithConstraints وLazyColumn وLazyRow هي استثناءات ملحوظة، حيث يعتمد تكوين العناصر التابعة على مرحلة التنسيق للعنصر الرئيسي.
من الناحية النظرية، تحدث كل مرحلة من هذه المراحل لكل إطار، ولكن لتحسين الأداء، يتجنّب Compose تكرار العمل الذي سيؤدي إلى حساب النتائج نفسها من المدخلات نفسها في كل هذه المراحل. تتجاهل Compose تخطي تشغيل دالة مركّبة إذا كان بإمكانها إعادة استخدام نتيجة سابقة، ولا تعيد واجهة مستخدم Compose تخطيط الشجرة بأكملها أو إعادة رسمها إذا لم يكن ذلك ضروريًا. لا ينفّذ Compose سوى الحد الأدنى من العمل المطلوب لتعديل واجهة المستخدم. تتوفّر إمكانية التحسين هذه لأنّ Compose يتتبّع عمليات قراءة الحالة ضمن المراحل المختلفة.
فهم المراحل
يوضّح هذا القسم بالتفصيل كيفية تنفيذ مراحل Compose الثلاث للمكوّنات القابلة للإنشاء.
مقطوعة موسيقية
في مرحلة الإنشاء، ينفّذ وقت تشغيل Compose الدوال البرمجية القابلة للإنشاء ويعرض بنية شجرية تمثّل واجهة المستخدم. تتألف شجرة واجهة المستخدم هذه من عقد تخطيط تحتوي على جميع المعلومات اللازمة للمراحل التالية، كما هو موضّح في الفيديو التالي:
الشكل 2: الشجرة التي تمثّل واجهة المستخدم والتي يتم إنشاؤها في مرحلة الإنشاء.
يبدو القسم الفرعي من شجرة الرموز البرمجية وواجهة المستخدم على النحو التالي:
في هذه الأمثلة، يتم ربط كل دالة مركّبة في الرمز البرمجي بعُقدة تصميم واحدة في شجرة واجهة المستخدم. في الأمثلة الأكثر تعقيدًا، يمكن أن تحتوي العناصر القابلة للإنشاء على منطق وتدفق تحكّم، ويمكن أن تنتج شجرة مختلفة حسب الحالات المختلفة.
التنسيق
في مرحلة التنسيق، تستخدم Compose شجرة واجهة المستخدم التي تم إنشاؤها في مرحلة الإنشاء كمدخل. تحتوي مجموعة عقد التنسيق على جميع المعلومات اللازمة لتحديد حجم كل عقدة وموقعها في المساحة الثنائية الأبعاد.
الشكل 4. قياس موضع كل عقدة تخطيط في شجرة واجهة المستخدم أثناء مرحلة التخطيط
أثناء مرحلة التنسيق، يتم الانتقال عبر الشجرة باستخدام الخوارزمية التالية المكوّنة من ثلاث خطوات:
- قياس العناصر الفرعية: تقيس العُقدة عناصرها الفرعية إذا كانت متوفّرة.
- تحديد الحجم: استنادًا إلى هذه القياسات، تحدّد العُقدة حجمها.
- وضع العناصر التابعة: يتم وضع كل عنصر تابع بالنسبة إلى موضع العنصر الرئيسي.
في نهاية هذه المرحلة، يحتوي كل عقدة تخطيط على ما يلي:
- العرض والارتفاع المحدّدان
- إحداثيات 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 في State وتعديله. لا يتم استدعاء دوال 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 UI سوى مرحلة الرسم.
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() يستخدم المعدِّل offset لتحديد موضع التنسيق النهائي، ما يؤدي إلى ظهور تأثير اختلاف المنظر أثناء تنقّل المستخدم.
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 إلا إلى إعادة تشغيل مرحلتَي التنسيق والرسم.
بالطبع، غالبًا ما يكون من الضروري تمامًا قراءة الحالات في مرحلة الإنشاء. ومع ذلك، هناك حالات يمكنك فيها تقليل عدد عمليات إعادة الإنشاء من خلال فلترة تغييرات الحالة. لمزيد من المعلومات حول هذا الموضوع،
يُرجى الاطّلاع على 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 ليصبح الارتفاع الفعلي للصورة. يتم إنشاء تركيبة ثم تتم جدولة إعادة إنشاء للّقطة التالية. ومع ذلك، خلال مرحلة الرسم الحالية، يتم عرض النص مع مساحة متروكة تبلغ 0، لأنّ قيمة imageHeightPx المعدَّلة لم تظهر بعد.
تركيب الإطار الثاني
تبدأ Compose الإطار الثاني، ويتم تشغيله بسبب التغيير في قيمة imageHeightPx. في مرحلة الإنشاء ضمن هذا الإطار، تتم قراءة الحالة ضمن Box
كتلة المحتوى. يتم الآن توفير مساحة فارغة للنص تتطابق بدقة مع ارتفاع الصورة. أثناء مرحلة التنسيق، يتم ضبط imageHeightPx مرة أخرى، ولكن لا تتم جدولة أي إعادة إنشاء أخرى لأنّ القيمة تظل ثابتة.
قد يبدو هذا المثال مصطنعًا، ولكن احذر من هذا النمط العام:
Modifier.onSizeChanged()أوonGloballyPositioned()أو بعض عمليات التنسيق الأخرى- تعديل بعض الحالات
- استخدِم هذه الحالة كإدخال لمعدِّل التنسيق (
padding()أوheight()أو ما شابه ذلك). - يُحتمل أن يكون مكرّرًا
يتم إصلاح المثال السابق باستخدام عناصر التنسيق الأساسية المناسبة. يمكن تنفيذ المثال السابق باستخدام Column()، ولكن قد يكون لديك مثال أكثر تعقيدًا يتطلّب شيئًا مخصّصًا، ما يستلزم كتابة تنسيق مخصّص. راجِع دليل التنسيقات المخصّصة للحصول على مزيد من المعلومات.
المبدأ العام هنا هو توفير مصدر واحد للحقيقة لعناصر متعدّدة في واجهة المستخدم يجب قياسها ووضعها بالنسبة إلى بعضها البعض. يعني استخدام عنصر أساسي مناسب للتنسيق أو إنشاء تنسيق مخصّص أنّ العنصر الرئيسي المشترَك الأدنى يعمل كمصدر موثوق يمكنه تنسيق العلاقة بين عناصر متعددة. يؤدي تقديم حالة ديناميكية إلى مخالفة هذا المبدأ.
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة.
- الحالة وJetpack Compose
- القوائم والجداول
- Kotlin لـ Jetpack Compose