ينفّذ Compose إطارًا في ثلاث مراحل مرتّبة بدقة وتتدفق للأمام:
- الإنشاء: يتم تشغيل دوال
@Composableلإنشاء شجرة واجهة المستخدم وتعديلها. - التنسيق: يقيس العناصر الفرعية ثم يضعها.
- الرسم: يصدر أوامر رسم اللوحة لعرض وحدات البكسل على الشاشة.
عندما تتم قراءة Compose State خلال أي مرحلة، يسجّل Compose تلقائيًا تبعية بين تلك الحالة والمرحلة المقابلة.
ما الذي يجعل الكتابة "عكسية"؟
تحدث الكتابة العكسية عندما يتم تعديل حالة في مرحلة لاحقة (أو في نطاق لاحق، أي نطاق مركّب يتم تنفيذه لاحقًا بالترتيب ضمن مسار التركيب نفسه) مقارنةً بالمكان الذي تمت قراءته فيه، ما يجبر Compose على إعادة التركيب من خلال جدولة مرحلة سابقة أو دالة مركّبة ليتم تشغيلها مرة أخرى. الكتابة العكسية هي حلقة إعادة تركيب غير محسّنة.
عواقب عمليات الكتابة السابقة
لا تُعدّ عمليات الكتابة العكسية أمرًا سيئًا بالضرورة، ولا تؤدي دائمًا إلى حدوث أعطال، ولكنّها غير فعّالة ويمكن أن تضرّ بأداء التطبيق بعدة طرق:
- عرض الإطارات الإضافية والإطارات التي تم إسقاطها: تفرض الكتابة العكسية على Compose تنفيذ عمليات إنشاء متكررة عبر إطارات متتالية، ما يؤدي إلى إهدار موارد وحدة المعالجة المركزية ووحدة معالجة الرسومات، وقد يتسبب في حدوث بطء في الأداء.
- مشاكل صحة الإطار الأول: إذا كان المكوّن يتطلب كتابة عكسية لحل أبعاده أو حالته النهائية، يتم عرض الإطار الأول ببيانات غير صالحة أو تلقائية أو غير مستقرة (مثل الحجم الصفري أو الإزاحة غير الصحيحة). يؤدي ذلك إلى ظهور وميض مرئي أو وميض في التنسيق عند عرض الإطار الثاني.
- حلقات إعادة التركيب اللانهائية: إذا أدى تغيير الحالة إلى تغيير حجم التنسيق، وكان حجم التنسيق يكتب باستمرار قيمة جديدة للحالة، فقد تخاطر بإنشاء حلقة إطار لا نهائية حيث تقوم الشاشة باستمرار بإعادة تركيب كل إطار دون أن تستقر أبدًا.
التدفق الأمامي عبر المراحل
يجب أن تتدفق تغييرات الحالة دائمًا للأمام خلال المراحل:
| مرحلة القراءة | كتابة السياق | هل هذا مقبول؟ | السبب |
|---|---|---|---|
التصميم (Modifier.offset { }) |
التكوين | نعم | تعدّل عملية الإنشاء الحالة ← يقرأها التنسيق لاحقًا في الإطار نفسه بدون إعادة الإنشاء. |
تعادُل (graphicsLayer { }، drawBehind { }) |
التكوين | نعم | تعدّل التركيبة الحالة ← يقرأها Draw في المرحلة النهائية. يتم تخطّي "التكوين والتنسيق" بالكامل. |
| الرسم | التصميم | نعم | يعدّل التنسيق الحالة، ويقرأها الرسم، وهو مسار صالح. |
| التكوين | يؤدي ردّ الاتصال بالحدث (onClick، onValueChange) إلى تغيير حالة القيادة. ملاحظة: لا يتم احتساب عمليات استدعاء التنسيق كأحداث. |
نعم | يؤدي الحدث إلى تغيير الحالة التي يتم استخدامها لتشغيل عملية الإنشاء. إذا حدث الحدث خارج الإطار (ليس في التكوين أو التخطيط أو الرسم)، فهذا صالح. |
| التكوين | Coroutine (LaunchedEffect) |
نعم، مع توخّي الحذر | يتم تعديل الحالة بشكل غير متزامن استجابةً لدورة الحياة أو الأحداث. قد تكون عمليات الكتابة من التأثيرات صالحة، ولكنّها قد تشير إلى طبقات حالة غير فعّالة. ويجب تجنُّبها قدر الإمكان. |
| موضع الإعلان (في التنسيق) | القياس (في التنسيق) | نعم | في Layout، من المقبول تعديل الحالة ثم قراءتها في الموضع. |
| القياس (في التنسيق) | موضع الإعلان (في التنسيق) | لا - الكتابة من اليسار إلى اليمين | تؤدي الكتابة إلى الحالة في الموضع الذي يكون بعد ذلك في حالة القراءة إلى حدوث حلقة إعادة قياس. |
| التكوين | التصميم (onSizeChanged، LayoutModifier) |
لا: الكتابة من الخلف إلى الأمام | يؤدي التنسيق إلى إبطال حلقة "الإنشاء" ← "إعادة الإنشاء". |
| التكوين | تعادُل (drawWithContent، Canvas) |
لا - الكتابة من اليسار إلى اليمين | تؤدي عملية الرسم إلى إبطال حلقة "الإنشاء" ← "إعادة الإنشاء". |
مجموعات المراحل: الخلفية مقابل الأمامية
في ما يلي أمثلة على عمليات الكتابة العكسية في Compose وكيفية حلّها.
العودة إلى الوراء: القراءة في Composition، والكتابة في Layout
- ما يحدث: يقرأ Composition السمة
componentHeightلتحديد واجهة المستخدم التي سيتم عرضها. في وقت لاحق من الإطار، تقيس مرحلة التنسيق طرق العرض أو تضعها وتكتب قيمة جديدة فيcomponentHeight(على سبيل المثال، باستخدامonSizeChangedأوonGloballyPositionedأوLayoutModifierمخصّص). - النتيجة: يؤدي تعديل
componentHeightفي "التنسيق" إلى إلغاء صلاحية مرحلة "التركيب" التي اكتملت للتو. يُرجى العِلم أنّonSizeChangedيعرض الحجم بعد اكتمال عملية قياس التنسيق. إذا استقرت قيمة الحالة المعدَّلة في المرة التالية، قد تتوقف إعادة الإنشاء بعد إطار واحد إضافي، ولكن إذا استمرت القيمة الجديدة في تغيير الحجم، فسيؤدي ذلك إلى تكرار الإطار بشكل لا نهائي. علاوةً على ذلك، يتم تشغيلonGloballyPositionedبعد كل من التخطيط والموضع، مما يجعل عمليات كتابة الحالة بداخله أكثر عرضة لإعادة التركيب المستمر وحلقات إعادة التخطيط عبر الإطارات المتتالية.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
الرجوع إلى الخلف: القراءة في "التأليف" والكتابة في "الرسم"
- ما يحدث: تتم قراءة الحالة في نص الدالة المركّبة (مرحلة الإنشاء)، ولكن يتم تغييرها داخل
Modifier.drawWithContentأوModifier.drawBehindأوCanvas(مرحلة الرسم). - النتيجة: تؤدي مرحلة الرسم إلى تغيير الحالة ← إلغاء صلاحية التركيب ← حلقة تكرار لا نهائية.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
الرجوع: القراءة في مرحلة "التأليف"، والكتابة في مرحلة "التأليف" (المرحلة نفسها)
- ما يحدث: تتم قراءة
countفي دالة مركّبة، ويتم تعديلcountمباشرةً في خانة محتوى أخرى للدوال المركّبة بعد قراءتها. - النتيجة: يسجّل نظام اللقطات القراءة والكتابة اللاحقة ضمن مسار الإنشاء نفسه، ما يؤدي إلى إبطال النطاق الحالي على الفور.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
القواعد الأساسية لمنع الكتابة من اليسار إلى اليمين
- لا تكتب في الحالة في
onGloballyPositionedأوonSizeChangedأوonPlacedأوonLayoutRectChangedأوLayoutModifierإذا تمت قراءة هذه الحالة في Composition لأنّ ذلك يتسبب في مشكلة صحة الإطار الأول.- إذا كانت إحداثيات التنسيق أو أحجامه مطلوبة فقط للرسم المخصّص، يمكنك قراءتها مباشرةً في مرحلة الرسم أو التنسيق (على سبيل المثال، باستخدام
Modifier.drawWithCacheأوModifier.layout). - بالنسبة إلى تحديد الحجم على مستوى النافذة (
WindowWidthSizeClass): يجب رفع مستوى رصد الحجم إلى مستوى النافذة. يتفرّع Composition إلى فئات حجم النافذة قبل إجراء القياس المحلي. - الحفاظ على اتساق التركيب: استخدِم تنسيقًا مخصّصًا واحدًا أو مكوّنات مثل
FlowRowأوLazyVerticalGridلضبط القياس والموضع خلال المرحلة الثانية بدون إعادة تركيب أو تغيير الحالة المستخدَمة في التركيب. - استخدام التركيب الفرعي: استخدِم
BoxWithConstraintsأوSubcomposeLayoutعندما يجب أن تتفرّع الدوال المركّبة الفرعية استنادًا إلى العرض أو الارتفاع المحلي. يجب توخي الحذر، لأنّ التركيب الفرعي يؤثر في الأداء ويمكن تجنّبه عادةً. - كحلّ أخير: يمكنك السماح بأن يكون الإطار الأول خاطئًا، مع تخزين الحجم في
onSizeChangedلتشغيل إعادة تركيب الإطار الثاني. يؤدي ذلك إلى ظهور تخطيط مرئي، وتأخُّر في الاستجابة، ومخاطر حدوث تكرار حلقي لا نهائي.
- إذا كانت إحداثيات التنسيق أو أحجامه مطلوبة فقط للرسم المخصّص، يمكنك قراءتها مباشرةً في مرحلة الرسم أو التنسيق (على سبيل المثال، باستخدام
- عدم تغيير الحالة بعد قراءتها للمرة الأولى في التركيب:
- على الرغم من أنّه يمكنك الكتابة بأمان في عناصر
MutableStateأثناء الإنشاء خارجSideEffect، يجب الحرص على عدم الكتابة في حالة سبق لك قراءتها في الإنشاء. ننصحك باستخدامrememberUpdatedStateعند الحاجة إلى كتابة حالة في دالة الإنشاء. إنّ الكتابة إلى حالة أثناء الإنشاء بطريقة أخرى تشير عادةً إلى تأثير مفقود أو حالة أو دالة مركّبة تم تصميمها بشكل غير مناسب. تذكَّر أنّ التركيب متفائل ويتم تنفيذه دائمًا بأحدث قيمة للحالة، لذا قد لا تظهر لك جميع تغييرات الحالة في عمليات إعادة التركيب. يجب عدم استخدام تحديثات واجهة المستخدم كطريقة للتعامل مع الأحداث التي تحدث لمرة واحدة، ما يجعل من غير المعتاد أن تتطلب قيمة الحالة التحديث نتيجة إعادة الإنشاء. - تجنَّب تغيير الحالات التي تتم ملاحظتها خارج التركيب (على سبيل المثال، حقول
ViewModelأو علاماتisVisible). عمليات كتابة الحالة التي تؤثر في التركيب تنتمي إلى دوال lambda للأحداث (onClick) أو الكوروتينات (LaunchedEffect) أو الآثار الجانبية (SideEffect). يُعدّrememberUpdatedStateاستثناءً لأنّه مصمّم لتغيير الحالة التي يتم استخدامها فقط في نص@Composable.
- على الرغم من أنّه يمكنك الكتابة بأمان في عناصر
- تأجيل قراءة الحالة إلى آخر مرحلة ممكنة:
- تضمن قراءة الحالات في مرحلة "الرسم" (
Modifier.graphicsLayer { alpha = ... }) أو "التنسيق" (Modifier.offset { IntOffset(...) }) أنّ التغييرات تؤدي فقط إلى إبطال المرحلة 2 أو 3، مع تخطّي المرحلة 1 (التركيب) بالكامل.
- تضمن قراءة الحالات في مرحلة "الرسم" (