SideEffect — ما هو، مزامنة الحالة في Jetpack Compose

المؤلف: IT Sectr نُشر: 2026-06-30 وقت القراءة: 9 دق

SideEffect هي دالة composable في Jetpack Compose تنفذ كتلة الكود الممررة عند كل إعادة تركيب ناجحة. على عكس LaunchedEffect و DisposableEffect، SideEffect غير مرتبط بالمفاتيح وليس له كتلة تنظيف — هو ببساطة يزامن حالة Compose مع الأنظمة الخارجية بعد كل عرض. هذا يجعله مثالياً لتحديث دوال callback، المزامنة مع ViewPager ونقل البيانات إلى Analytics SDK. وفقًا لـ Android Developers Documentation (2025)، يتم تنفيذ SideEffect بعد أن يؤكد Compose إعادة تركيب ناجحة، ولا يتم تنفيذه إذا تم تخطي إعادة التركيب.

النقاط الرئيسية

  • SideEffect — API تأثير جانبي للكود المُنفّذ بعد كل إعادة تركيب ناجحة.
  • المزامنة — ينقل حالة Compose إلى أنظمة خارجية لا تدعم Compose.
  • بدون مفاتيح — على عكس LaunchedEffect، SideEffect لا يعاد تشغيله بل يتم تنفيذه عند كل إعادة تركيب.
  • لا تنظيف — SideEffect لا يوفر onDispose، وهو مصمم فقط للمزامنة أحادية الاتجاه.
  • متزامن — يتم تنفيذ الكتلة بشكل متزامن داخل مرحلة التركيب في Compose، بدون مجريات مشتركة.

ما هو SideEffect في Jetpack Compose

SideEffect هو أبسط API تأثير جانبي في Jetpack Compose. يقوم بتنفيذ كتلة كود عند كل إعادة تركيب ناجحة لمكون composable. كلمة “ناجحة” هي المفتاح هنا: إذا قرر Compose أن إعادة التركيب غير مطلوبة (على سبيل المثال، جميع معلمات الإدخال لم تتغير وسيكون النتيجة نفسها)، فإن SideEffect لا يتم تنفيذه. يضمن ذلك أنه يتم استدعاء كتلة المزامنة فقط عندما تتغير واجهة المستخدم فعلياً.

حالة الاستخدام الرئيسية لـ SideEffect هي مزامنة حالة Compose مع الكائنات التي ليست جزءًا من شجرة Compose. تشمل الأمثلة النموذجية: تحديث دالة callback في نظام Legacy View، نقل الحالة الحالية إلى ViewPager، إرسال حدث إلى SDK تحليلي عندما تتغير البيانات المعروضة، والمزامنة مع SDK الخرائط التي تتوقع تحديثات بتنسيق خارجي.

وفقًا لـ Android Developer Blog (2025)، غالبًا ما يتم استخدام SideEffect مع remember: remember يحافظ على كائن (مثل callback)، و SideEffect يقوم بتحديثه عندما تتغير أي تابعية. هذا النمط مهم بشكل خاص للمكتبات التي تقبل كائنات مستمعة ولا تعيد إنشائها عند التحديث — بدون SideEffect، سيحتوي المستمع على مرجع قديم للحالة الحالية.

kotlin
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setZoom(zoomLevel)
        mapView.updateMarkers(markers)
    }
    
    AndroidView(factory = { mapView })
}

كيف يعمل SideEffect ومراحل التركيب

لفهم SideEffect، تحتاج إلى فهم مراحل تنفيذ Jetpack Compose. يمر Compose بثلاث مراحل لكل إطار: Composition (ما سيتم عرضه)، Layout (أين سيتم عرضه)، Drawing (كيف سيتم عرضه). يتم تنفيذ SideEffect في نهاية مرحلة Composition — بعد أن تكون جميع دوال composable قد عملت، ولكن قبل مرحلة Layout. يضمن ذلك أن SideEffect يرى الحالة النهائية لجميع المتغيرات بعد إعادة التركيب.

يمنح هذا الموقع في دورة الحياة ميزة مهمة: SideEffect لا يمكنه التسبب في إعادة تركيب لا نهائية، حتى لو تم تغيير الحالة داخله. نظرًا لأنه يتم تنفيذه بعد التركيب، فإن التغييرات التي تُجرى داخل SideEffect ستُطبق فقط في الإطار التالي — هذا يمنع الدورات التي تحدث عادة عند تغيير الحالة داخل جسم دالة composable (عندما يؤدي setState داخل التركيب إلى إعادة تركيب جديدة قبل انتهاء الحالية).

ميزة أخرى هي أن SideEffect لا يتم تحسينه بالمفاتيح. يتم تنفيذه عند كل إعادة تركيب بغض النظر عن الحالة المحددة التي تغيرت. إذا كنت تحتاج تحكمًا أكثر دقة (تنفيذ فقط عند تغيير معلم محدد)، استخدم LaunchedEffect مع مفاتيح أو الفف SideEffect في فحص تغيير من خلال remember.

kotlin
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }

SideEffect {
    if (currentZoom != zoomLevel) {
        map.animateToZoom(zoomLevel)
        currentZoom = zoomLevel
    }
}

// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change

تحديث دوال callback باستخدام SideEffect

أكثر السيناريوهات العملية شيوعًا لـ SideEffect هي تحديث دوال callback التي تلتقط الحالة الحالية. في Jetpack Compose يسمى هذا “إدارة دورة حياة callback”. المشكلة هي أن تعابير lambda في Kotlin تلتقط المتغيرات بالمرجع، وإذا تم إنشاء callback بقيمة واحدة ثم تغيرت المتغيرة — فإن callback يستمر في استخدام القيمة القديمة.

تأمل مثالًا: SDK خرائط Google Maps لنظام Android يقبل كائن OnCameraMoveListener عبر setOnCameraMoveListener(). إذا قمت بتمرير lambda تلتقط isTrackingEnabled، فعندما يتغير isTrackingEnabled لن يتم تحديث lambda — سيستمر SDK الخرائط في استدعاء callback القديم ببيانات قديمة. SideEffect يحل هذه المشكلة: يقوم بإعادة تعيين المستمع عند كل إعادة تركيب، مما يضمن أن SDK يستخدم دائمًا lambda الحالية مع أحدث حالة.

وفقًا لـ وثائق Maps SDK لنظام Android (2025)، توصي Google بهذا النمط بالضبط عند دمج Maps مع Jetpack Compose. يتم استخدام نهج مماثل لـ WebView، VideoView، TextureView وأي مكونات أخرى قائمة على View تقبل callbacks عبر طرق set. SideEffect يضمن أن callbacks تكون محدثة مع كل تغيير في الحالة.

kotlin
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setOnMarkerClickListener { marker ->
            onMarkerClick(marker)
            true
        }
        mapView.isTrafficEnabled = isTrackingEnabled
    }
    
    AndroidView(factory = { mapView })
}

المزامنة مع Analytics SDK

سيناريو مهم آخر لـ SideEffect هو إرسال الأحداث إلى أنظمة التحليل عندما تتغير حالة واجهة المستخدم. على سبيل المثال، عندما يبدل المستخدم العلامات التبويبية في TabLayout داخل شاشة Compose، يمكن SideEffect نقل حالة العلامة المحددة حالياً إلى Firebase Analytics أو AppsFlyer. كل مرة تتغير العلامة المحددة (وتحدث إعادة التركيب)، يرسل SideEffect الحدث المناسب.

الفرق عن إرسال الأحداث مباشرة في onClick أو onTabSelected هو أن SideEffect ينطلق عند تغييرات الحالة من أي مصدر — ليس فقط إجراءات المستخدم ولكن أيضًا التغييرات البرمجية، استعادة الحالة بعد تدوير الشاشة أو الروابط العميقة. هذا يجعل SideEffect آلية مزامنة عالمية مستقلة عن مصدر التغيير.

وفقًا لـ Firebase Best Practices (Google, 2025)، فإن إرسال أحداث التحليل عبر SideEffect يوفر صورة أكثر اكتمالاً لرحلة المستخدم، حيث يلتقط جميع تغييرات الحالة، بما في ذلك تلك التي تحدث بدون إجراء مباشر من المستخدم. ولكن من المهم عدم المبالغة: كل حدث تحليلي هو طلب شبكة، لذا فالحالات المتغيرة بشكل متكرر (موقع التمرير، إحداثيات الأصابع) SideEffect غير مناسب — استخدم debounce أو أرسل الأحداث فقط عند التغييرات الهامة.

kotlin
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
    val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
    
    SideEffect {
        val params = Bundle().apply {
            putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
            putString(FirebaseAnalytics.Param.ITEM_ID, productId)
        }
        firebaseAnalytics.logEvent(        FirebaseAnalytics.Event.VIEW_ITEM, params)
    }
    
    // UI with TabRow and selected tab
}

SideEffect مقارنةً بـ LaunchedEffect: متى نستخدم كلاً

يعتمد الاختيار بين SideEffect و LaunchedEffect على عاملين: هل تحتاج إلى تنفيذ غير متزامن وهل تحتاج إلى تحكم قائم على المفاتيح. SideEffect متزامن ويتم تنفيذه عند كل إعادة تركيب. LaunchedEffect غير متزامن (مجري مشترك) ويتم تنفيذه فقط عندما تتغير المفتاح، لا في كل إعادة تركيب.

الخاصيةSideEffectLaunchedEffect
التنفيذعند كل إعادة تركيبعند تغيير المفتاح
غير متزامنمتزامنمجري مشترك
المفاتيحلانعم (vararg)
التنظيفلاإلغاء تلقائي للمجري المشترك
الاستخدام النموذجيCallbacks، Analytics، مزامنة العرضالتحميل، الاشتراك في Flow، المؤقتات

في الممارسة، 70% من حالات استخدام التأثيرات الجانبية تغطيها LaunchedEffect (العمليات غير المتزامنة، تحميل البيانات)، 20% DisposableEffect (الموارد مع التنظيف) وفقط 10% SideEffect (مزامنة callbacks). SideEffect هو أداة متخصصة لمجموعة محدودة من المهام، ولكنه في تلك المهام لا غنى عنه.

الأخطاء الشائعة مع SideEffect

الخطأ الرئيسي هو تغيير حالة Compose داخل SideEffect. على الرغم من أن SideEffect لا يتسبب في حلقة لا نهائية مباشرة (لأنه يتم تنفيذه بعد مرحلة التركيب)، لكنه يمكنه إثارة إعادات تركيب مفرطة. إذا تم تغيير الحالة داخل SideEffect (mutableStateOf)، فإنه يولد إعادة تركيب جديدة في الإطار التالي، والتي تنفذ SideEffect مرة أخرى — وهكذا حتى الاستقرار. هذا ليس حلقة لا نهائية، ولكنه عمل غير ضروري للإطار.

الخطأ الثاني هو تنفيذ عمليات حسابية ثقيلة داخل SideEffect. نظرًا لأن SideEffect يتم استدعاؤه عند كل إعادة تركيب، ويمكن أن تحدث إعادات التركيب عشرات المرات في الثانية (أثناء الرسوم المتحركة، التمرير)، فأي كود ثقيل داخل SideEffect سيؤدي إلى تسقط الإطارات. انقل العمليات الثقيلة خارج التركيب — إلى مجري مشترك (LaunchedEffect) أو احسب عبر derivedStateOf / remember.

الخطأ الثالث هو محاولة استخدام SideEffect لكود غير متزامن. SideEffect ليس دالة suspend، لذا فإن delay()، await()، collect() وغيرها من عمليات المجري المشترك داخله لن تترجم. إذا كنت تحتاج إلى تنفيذ إجراء غير متزامن بعد إعادة التركيب، استخدم snapshotFlow { ... } بالاشتراك مع LaunchedEffect، أو أطلق مجريًا مشتركًا عبر rememberCoroutineScope.

الأسئلة الشائعة

هل يتم تنفيذ SideEffect عند أول تركيب؟

نعم، SideEffect يتم تنفيذه عند كل تركيب ناجح، بما في ذلك أول تركيب — عندما يظهر المكون لأول مرة على الشاشة. هذا يختلف عن LaunchedEffect(Unit)، الذي يتم تنفيذه أيضًا مرة واحدة عند أول تركيب ولكنه لا يتم تنفيذه في إعادات التركيب اللاحقة (إذا لم يتغير المفتاح).

هل يمكن لـ SideEffect أن يتسبب في حلقة لا نهائية؟

لا، SideEffect يتم تنفيذه بعد مرحلة التركيب — التغييرات التي تُجرى داخله ستُطبق فقط في الإطار التالي، مما يمنع الدورات. ولكن تغيير الحالة بشكل متكرر داخل SideEffect يمكن أن يتسبب في موجة من إعادات التركيب، مما يقلل الأداء. قم بتغيير الحالة داخل SideEffect فقط عندما تكون ضرورة حقيقية.

ما الفرق بين SideEffect و snapshotFlow؟

SideEffect يتم تنفيذه بشكل متزامن عند كل إعادة تركيب. snapshotFlow ينشئ Flow من حالة Compose ويمكن استخدامه مع collectLatest في LaunchedEffect لمعالجة التغييرات بشكل تفاعلي. snapshotFlow مناسب للحالات التي تحتاج إلى الرد على التغييرات مع debounce، filter أو distinctUntilChanged — وهو ما هو غير ممكن في SideEffect المتزامن.

كيف أن تصلح SideEffect إذا كان يتم تنفيذه بشكل متكرر جداً؟

استخدم Android Studio Compose Modifier Debugger أو أضف تسجيلات باسم المكون وترداد الاستدعاء. إذا كان SideEffect يتم تنفيذه بشكل متكرر أكثر مما هو متوقع، تحقق من عدم تغيير حالة المكون الأصلي بدون ضرورة. التحسين: استخرج الأجزاء المستقرة من واجهة المستخدم إلى دوال composable منفصلة مع تعليقات unstable لتقليل عدد إعادات التركيب.

هل يمكن دمج SideEffect مع DisposableEffect؟

نعم، يمكن استخدامهما في نفس المكون لأغراض مختلفة. DisposableEffect مسؤول عن إعداد وتنظيف المورد (مرة واحدة)، بينما SideEffect يتولى مزامنة الحالة الحالية مع ذلك المورد عند كل إعادة تركيب. مثال نموذجي: DisposableEffect يسجل callback عبر API، و SideEffect يقوم بتحديث البيانات الملتقطة في ذلك callback عند كل تغيير.

الملخص

  • SideEffect — API تأثير جانبي لكود متزامن يتم تنفيذه بعد كل إعادة تركيب ناجحة في Jetpack Compose.
  • المزامنة — حالة الاستخدام الرئيسية: نقل حالة Compose إلى أنظمة خارجية (Google Maps، WebView، ViewPager، Analytics SDK).
  • Callbacks — SideEffect يضمن أن دوال callback التي تلتقط الحالة الحالية تبقى محدثة مع كل تحديث لواجهة المستخدم.
  • بدون دورات — يتم تنفيذه بعد مرحلة التركيب، لذا فإن تغيير الحالة داخل SideEffect لا يتسبب في إعادة تركيب لا نهائية.
  • القيود — لا يدعم المفاتيح، غير المتزامن أو كتلة التنظيف، لهذه المهام استخدم LaunchedEffect أو DisposableEffect.
  • الأداء — تجنب العمليات الحسابية الثقيلة داخل SideEffect، لأنه يتم تنفيذه عند كل إعادة تركيب (حتى 60 مرة في الثانية).
  • التصحيح — تحكم في ترداد الاستدعاء عبر Compose Debugger وحسن باستخدام remember لتصفية إعادات التركيب غير الضرورية.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا