State Hoisting هو نمط في Jetpack Compose يتم فيه نقل الحالة من دالة Composable تابعة إلى الدالة الأصلية، بينما تستقبل التابعة البيانات عبر المعاملات وتُعلم بالتغييرات من خلال الاستدعاءات الخلفية. هذا تطبيق لمبدأ تدفق البيانات أحادي الاتجاه (UDF)، حيث تُرفع الحالة إلى الأعلى وتنزل الأحداث إلى الأسفل. وفقاً Google Android Developers, 2026، فإن State Hoisting يجعل المكونات قابلة لإعادة الاستخدام والاختبار والتنبؤ.
الرئيسية
State Hoisting هو نمط لا تمتلك فيه دالة Composable الحالة بل تستقبلها من الخارج. بدلاً من var داخل الدالة، يُستخدم معاملان: قيمة للعرض واستدعاء خلفي لامدا لمعالجة التغييرات. تقنياً، هذا يعني أن المكون التابع يصبح عديم الحالة (stateless)، بينما الأصل هو مالك الحالة (stateful).
مثال: مكون TextField من Material3 لا يخزن النص المدخل داخلياً. يستقبل value: String و onValueChange: (String) -> Unit. الأصل الذي يستدعي TextField يعلن var value by remember { mutableStateOf("") } ويمرر value و onValueChange. هذا هو State Hoisting التقليدي: TextField هو مكون بسيط (يعرض ويبلغ عن الإدخال)، والأصل ذكي (يمتلك الحالة).
عديم الحالة مقابل مالك الحالة: المكون عديم الحالة أسهل في الاختبار — لا يعتمد على حالة داخلية، سلوكه محدد بالكامل بواسطة معاملات الإدخال. المكون مالك الحالة مناسب للنمذجة السريعة لكن إعادة استخدامه أصعب: فهو مرتبط بشدة بمصدر بيانات واحد. State Hoisting يمنحك الخيار: أي مكون يمكن جعله عديم الحالة برفع الحالة إلى الأعلى.
UDF (تدفق البيانات أحادي الاتجاه) هو مبدأ معماري حيث تتحرك البيانات في اتجاه واحد: من مصدر الحقيقة (ViewModel أو Composable أصلي) إلى واجهة المستخدم، وتتدفق الأحداث في الاتجاه المعاكس. State Hoisting هو تطبيق UDF على مستوى المكونات الفردية. بدلاً من أن يقرر كل مكون متى وكيف يغير حالته الخاصة، فإنه يخبر الأصل عن حدث، والأصل يقرر كيفية تغيير الحالة.
مزايا UDF: قابلية التنبؤ — تتغير الحالة في مكان واحد فقط، مما يلغي حالات السباق؛ قابلية التتبع — سلسلة الاستدعاءات تسمح بإعادة بناء سلسلة التغييرات؛ الاختبار — يمكن استخراج منطق الحالة إلى فئة منفصلة واختباره دون واجهة مستخدم. في المشاريع الكبيرة، UDF مع State Hoisting هو المعيار الفعلي.
مصدر الحقيقة الوحيد (Single Source of Truth) هو مبدأ آخر يرافق UDF. لكل جزء من الحالة مصدر واحد بالضبط. إذا استخدم مكونان نفس الحالة، يجب مشاركة المصدر (على مستوى ViewModel أو الأصل المشترك). State Hoisting يضمن أن المصدر في الأعلى في التسلسل الهرمي ولا يحدث ازدواج في الحالة.
| الاتجاه | ما يتم تمريره | كيف يتم التنفيذ |
|---|---|---|
| لأسفل (أصل ← تابع) | قيمة للعرض | معامل value: T |
| لأعلى (تابع ← أصل) | حدث التغيير | معامل onValueChange: (T) -> Unit |
القاعدة الرئيسية: يجب رفع الحالة إلى أدنى مستوى ممكن يكفي لجميع المكونات التي تحتاجها. إذا كانت الحالة تُستخدم فقط داخل مكون واحد — أبقها محلية. إذا كان مكونان متجاوران يحتاجان نفس الحالة — ارفعها إلى الأصل المشترك. إذا كانت الحالة مطلوبة عبر الشاشة بأكملها — ارفعها إلى ViewModel.
قاعدة الرفع الأدنى تمنع التعقيد غير الضروري. لا فائدة من رفع حالة حقل نصي إلى ViewModel إذا كانت تُستخدم فقط داخل شاشة واحدة ولا يتم حفظها عند إعادة إنشاء Activity. استخدم rememberSaveable على مستوى أصل الشاشة، وليس ViewModel، لحالة واجهة المستخدم التي يجب أن تبقى بعد تدوير الشاشة ولكن لا تحتاجها منطق الأعمال.
متى نرفع إلى ViewModel: إذا كانت الحالة يجب أن تبقى عند إعادة إنشاء Activity، أو إذا كانت ضرورية لعدة شاشات، أو إذا كان تغيير الحالة يشغل منطق أعمال (طلبات شبكة، قاعدة بيانات). State Hoisting على مستوى ViewModel هو نمط قياسي في بنية MVVM، حيث طبقة واجهة المستخدم عديمة الحالة و ViewModel مالكة الحالة.
// ❌ سيء: المكون يمتلك حالته الخاصة
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ جيد: State Hoisting — الحالة في الأصل
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// الاستخدام: الأصل يمتلك الحالة
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
لنأخذ شاشة تسجيل الدخول مع حقلين (البريد الإلكتروني، كلمة المرور) وزر. جميع المكونات الثلاثة تستقبل الحالة عبر State Hoisting: البريد الإلكتروني وكلمة المرور يديرهما الأصل، والزر يستقبل حالة التمكين كقيمة.
// State Hoisting على مستوى الشاشة
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// حقل البريد الإلكتروني — State Hoisting عبر لامدا
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// حقل كلمة المرور — بالمثل
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// الزر — يستقبل فقط enabled (للقراءة فقط)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// مكون عديم الحالة: يستقبل بريداً إلكترونياً + استدعاء خلفي
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("البريد الإلكتروني") },
singleLine = true
)
}
// مكون زر عديم الحالة
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("تسجيل الدخول")
}
}
EmailField و PasswordField عديمَا الحالة تماماً. يمكن إعادة استخدامهما على أي شاشة بتوصيلهما بأي مصدر بيانات. LoginButton يستقبل enabled للقراءة فقط — وهذه شكل آخر من State Hoisting حيث لا تُرفع الحالة (الزر لا يمكنه تمكين نفسه) بل تُمرر جاهزة. هذا النهج يوفر أقصى مرونة مع أقل اقتران بين المكونات.
ليست كل حالة تحتاج إلى الرفع. الحالة المحلية (State داخل Composable) مبررة عندما: البيانات مطلوبة فقط داخل مكون واحد، لا تؤثر على العناصر المجاورة، ولا يجب أن تبقى بعد إعادة تركيب قسم معين. على سبيل المثال، حالة الرسوم المتحركة، تركيز حقل الإدخال، موضع التمرير الحالي — من المعقول إبقاؤها محلياً.
متى يكون State Hoisting ضرورياً: عندما تُستخدم الحالة بواسطة عدة مكونات تابعة؛ تغيير في أحد التوابع يجب أن ينعكس في آخر؛ منطق تغيير الحالة يحتاج إلى اختبار منفصل عن واجهة المستخدم؛ الحالة يجب أن تبقى عند إعادة إنشاء Activity. في هذه الحالات، الحالة المحلية تخلق ازدواجاً وتناقضاً في البيانات.
النهج المختلط: أبقِ الحد الأدنى من الحالة محلياً، وارفع الباقي. قاعدة Compose: «ارفع الحالة إلى أعلى مستوى ضروري وإلى أدنى مستوى ممكن». عملياً، هذا يعني البدء بـ remember المحلي، ورفع المستوى فقط عند الحاجة للوصول من مكون آخر. لا تطبق State Hoisting بشكل استباقي — فهذا يعقد الكود دون ضرورة.
الأسئلة الشائعة
State Hoisting هو نمط على مستوى مكونات واجهة المستخدم. ViewModel هي طبقة معمارية لمنطق الأعمال. State Hoisting يمكنه رفع الحالة إلى مستوى Composable الأصلي، أو مستوى الشاشة، أو إلى ViewModel. ViewModel هي أعلى نقطة رفع للحالة التي يجب أن تبقى عند إعادة إنشاء Activity.
المكون عديم الحالة يُختبر بمجرد تمرير القيم. استدعِ Composable بالمعاملات المطلوبة وتحقق من العرض عبر ComposeTestRule. تُختبر تغييرات الحالة على مستوى الأصل أو ViewModel — منفصلة عن واجهة المستخدم. هذا يبسط الاختبارات بشكل كبير: لا حاجة لمحاكاة إعادة التركيب داخل المكون.
نعم، هذه ممارسة شائعة. إذا كان المكون يحتاج فقط لعرض البيانات دون إمكانية تعديلها — مرر State<T> (للقراءة فقط). سيشترك المكون في التغييرات لكنه لن يستطيع بدءها. هذا يعزز التغليف ويحمي البيانات من التحولات غير المرغوب فيها.
للتمرير العميق استخدم CompositionLocal أو التمرير عبر معاملات Composable الأصلي. إذا كانت الحالة مطلوبة عبر الشاشة بأكملها — استخرجها إلى ViewModel واستخدم collectAsState(). التمرير عبر 5+ مستويات هو علامة على بنية غير صحيحة؛ أعد النظر في التسلسل الهرمي للمكونات.
State Hoisting قد يزيد قليلاً من عدد مرات إعادة التركيب، حيث أن التغيير في الأصل يمكن أن يعيد تركيب جميع التوابع. استخدم derivedStateOf لتصفية التغييرات و keys في LazyColumn للتحديثات الموجّهة. في معظم السيناريوهات، الحمل الإضافي لـ State Hoisting لا يُذكر مقارنة بفائدة قابلية الصيانة.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا