ينفّذ Compose إطارًا في ثلاث مراحل منظَّمة بدقة ومتسلسلة:
- الإنشاء: يتم تشغيل دوال
@Composableلإنشاء شجرة واجهة المستخدم وتعديلها. - التنسيق: يقيس الأطفال ثم يضعهم في المكان المناسب.
- الرسم: يرسل أوامر رسم اللوحة لعرض وحدات البكسل على الشاشة.
عندما يتم قراءة حالة State في أي مرحلة، تسجّل Compose تلقائيًا اعتمادية بين هذه الحالة والمرحلة المقابلة.
ما الذي يجعل عملية الكتابة "عكسية"؟
تحدث عملية كتابة عكسية عندما يتم تعديل حالة في مرحلة لاحقة (أو في نطاق تابع، أي نطاق قابل للإنشاء يتم تنفيذه لاحقًا بالترتيب ضمن عملية التركيب نفسها) مقارنةً بمكان قراءتها، ما يجبر Compose على إعادة التركيب من خلال جدولة مرحلة أو عنصر قابل للإنشاء سابق ليتم تنفيذه مرة أخرى. الكتابة الرجعية هي حلقة إعادة تركيب غير محسّنة.
عواقب عمليات الكتابة السابقة
لا تُعد عمليات الكتابة السابقة بالضرورة أمرًا سيئًا، ولا تؤدي دائمًا إلى حدوث أعطال، ولكنّها غير فعّالة ويمكن أن تؤثر سلبًا في أداء التطبيق بعدة طرق:
- عرض لقطات إضافية وإسقاط لقطات: تؤدي عملية الكتابة إلى الخلف إلى إجبار Compose على تنفيذ عمليات تركيب مكررة على مستوى اللقطات المتتالية، ما يؤدي إلى إهدار موارد وحدة المعالجة المركزية ووحدة معالجة الرسومات، وربما التسبب في حدوث إيقاف مؤقت لعرض واجهة المستخدم.
- مشاكل صحة الإطار الأول: إذا كان المكوّن يتطلّب عملية كتابة عكسية لحلّ مشكلة الأبعاد أو الحالة النهائية، سيتم عرض الإطار الأول ببيانات غير صالحة أو تلقائية أو غير مستقرة (مثل الحجم صفر أو الإزاحة غير الصحيحة). يؤدي ذلك إلى ظهور وميض مرئي أو وميض في التنسيق عند عرض الإطار الثاني.
- حلقات إعادة التركيب اللانهائية: إذا أدّى تغيير الحالة إلى تغيير حجم التنسيق، وكان حجم التنسيق يعيد كتابة قيمة جديدة إلى الحالة باستمرار، قد يؤدي ذلك إلى إنشاء حلقة إطارات لانهائية حيث تتم إعادة تركيب الشاشة باستمرار في كل إطار بدون أن تستقر أبدًا.
التنقل للأمام خلال المراحل
يجب أن تنتقل تغييرات الحالة دائمًا للأمام خلال المراحل:
| مرحلة القراءة | كتابة السياق | هل هو مقبول؟ | السبب |
|---|---|---|---|
التصميم (Modifier.offset { }) |
التركيبة | نعم | تعدّل Composition الحالة → يقرأ Layout الحالة لاحقًا في الإطار نفسه بدون إعادة الإنشاء. |
السحب (graphicsLayer { }، drawBehind { }) |
التركيبة | نعم | تعدّل المقطوعة الموسيقية الحالة → تقرأ أداة الرسم الحالة في المرحلة النهائية. يتم تخطّي التكوين والتنسيق بالكامل. |
| رسم | التصميم | نعم | يعدّل التنسيق الحالة، ويقرأ الرسم الحالة، وهو مسار صالح. |
| التركيبة | تغيير حالة العرض بسبب Event Callback (onClick، onValueChange) ملاحظة: لا يتم احتساب عمليات معاودة الاتصال بالتنسيق كأحداث. |
نعم | يؤدي الحدث إلى تغيير الحالة المستخدَمة لتنفيذ التركيب. إذا حدث الحدث خارج الإطار (ليس في "التركيب" أو "التصميم" أو "الرسم")، يكون ذلك صالحًا. |
| التركيبة | الكوروتين (LaunchedEffect) |
نعم، ولكن يجب توخّي الحذر | تعديل الحالة بشكل غير متزامن استجابةً لدورة الحياة/الأحداث يمكن أن تكون عمليات الكتابة من التأثيرات صالحة، ولكن يمكن أن تشير إلى عدم كفاءة في ترتيب الحالة. ويجب تجنُّبها قدر الإمكان. |
| موضع الإعلان (في التصميم) | القياس (في التنسيق) | نعم | في Layout، يمكن تعديل الحالة ثم قراءتها في موضع الإعلان. |
| القياس (في التنسيق) | موضع الإعلان (في التصميم) | لا - الكتابة للخلف | الكتابة إلى حالة في موضع الإعلان تكون لاحقًا في حالة القراءة، ما يؤدي إلى تكرار عملية إعادة القياس. |
| التركيبة | التصميم (onSizeChanged، LayoutModifier) |
لا - الكتابة من اليسار إلى اليمين | يؤدي عدم صحة التنسيق إلى تكرار عملية إعادة التكوين. |
| التركيبة | السحب (drawWithContent، Canvas) |
لا - الكتابة من اليسار إلى اليمين | يؤدي الرسم إلى إبطال صحة التركيب ← حلقة إعادة التركيب. |
مجموعات المراحل: التراجع مقابل التقدم
في ما يلي أمثلة على عمليات الكتابة السابقة في Compose وكيفية حلّها.
الرجوع: القراءة في "وضع الإنشاء" والكتابة في "وضع التنسيق"
- ما يحدث: تقرأ عملية الإنشاء
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أوLayoutModifierإذا تمت قراءة هذه الحالة في Composition لأنّ ذلك يؤدي إلى حدوث مشكلة في صحة الإطار الأول.- إذا كنت بحاجة إلى إحداثيات أو أحجام التنسيق للرسم المخصّص فقط، يمكنك قراءتها مباشرةً في مرحلة الرسم أو التنسيق (على سبيل المثال، باستخدام
Modifier.drawWithCacheأوModifier.layout). - بالنسبة إلى تحديد الحجم على مستوى النافذة (
WindowWidthSizeClass): يجب رفع مستوى الملاحظة المتعلقة بحجم الرافعة إلى مستوى النافذة. تتفرّع التركيبة حسب فئات حجم النافذة قبل إجراء القياس المحلي. - الحفاظ على اتساق المقطوعة الموسيقية: استخدِم تصميمًا مخصّصًا واحدًا أو مكوّنات مثل
FlowRowأوLazyVerticalGridالتي تضبط القياس والموضع خلال المرحلة 2 بدون إعادة إنشاء المقطوعة الموسيقية أو تغيير الحالة المستخدَمة في المقطوعة الموسيقية. - استخدام التركيب الفرعي: استخدِم
BoxWithConstraintsأوSubcomposeLayoutعندما يجب أن تتفرّع العناصر القابلة للإنشاء الفرعية استنادًا إلى العرض أو الارتفاع المحليين. يجب توخّي الحذر: تتسبّب التركيبة الفرعية في تكلفة أداء ويمكن عادةً تجنُّبها. - كحلّ أخير: اسمح بأن يكون الإطار الأول غير صحيح، مع تخزين الحجم في
onSizeChangedلتفعيل إعادة إنشاء الإطار الثاني. ويؤدي ذلك إلى ظهور تغييرات مفاجئة في التنسيق، وحدوث إيقاف مؤقت لعرض واجهة المستخدم، ومخاطر حدوث حلقات لا نهائية.
- إذا كنت بحاجة إلى إحداثيات أو أحجام التنسيق للرسم المخصّص فقط، يمكنك قراءتها مباشرةً في مرحلة الرسم أو التنسيق (على سبيل المثال، باستخدام
- عدم تغيير الحالة بعد قراءتها للمرة الأولى في التركيب:
- على الرغم من أنّه يمكنك الكتابة بأمان إلى عناصر
MutableStateأثناء الإنشاء خارجSideEffect، يجب توخّي الحذر الشديد لضمان عدم الكتابة إلى حالة ربما قرأتها سابقًا أثناء الإنشاء. ننصحك باستخدامrememberUpdatedStateعندما تنشأ الحاجة إلى كتابة حالة في التركيب. عادةً ما يشير تعديل حالة أثناء عملية التكوين بطريقة أخرى إلى عدم توفّر تأثير أو إلى تصميم غير مناسب للحالة أو الدالة المركّبة. تذكَّر أنّ التركيب متفائل ويتم تنفيذه دائمًا باستخدام أحدث قيمة للحالة، لذا قد لا تظهر لك جميع تغييرات الحالة في عمليات إعادة التركيب. لا يجب استخدام عمليات تعديل واجهة المستخدم كطريقة للتعامل مع الأحداث التي تحدث لمرة واحدة، ما يجعل من غير الشائع أن تتطلّب قيمة الحالة تعديلًا نتيجة إعادة التركيب. - تجنَّب تغيير الحالات التي يتم رصدها خارج التركيب (على سبيل المثال، حقول
ViewModelأو علاماتisVisible). تندرج عمليات كتابة الحالة التي تؤثّر في التركيب ضمن تعبيرات lambda الخاصة بالأحداث (onClick) أو إجراءات coroutines (LaunchedEffect) أو الآثار الجانبية (SideEffect). ويُعدrememberUpdatedStateاستثناءً لأنّه مصمّم لتغيير الحالة التي يتم استخدامها فقط في نص@Composable.
- على الرغم من أنّه يمكنك الكتابة بأمان إلى عناصر
- تأجيل عمليات قراءة الحالة إلى آخر مرحلة ممكنة:
- تضمن حالات القراءة في Draw (
Modifier.graphicsLayer { alpha = ... }) أو Layout (Modifier.offset { IntOffset(...) }) أنّ التغييرات تؤدي فقط إلى إبطال المرحلة 2 أو 3، مع تخطّي المرحلة 1 (التركيب) بالكامل.
- تضمن حالات القراءة في Draw (