SharedFlow: ما هو، SharedFlow vs StateFlow في Android

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

SharedFlow هو تدفق تفاعلي ساخن من مكتبة Kotlin Coroutines، مُحسَّن للأحداث لمرة واحدة (one-shot events) التي لا يجب أن تتكرر عند تدوير الشاشة أو إعادة إنشاء المشترك. نعرض كيف يختلف SharedFlow عن StateFlow: على عكس StateFlow، لا يخزن SharedFlow القيمة الأخيرة للمشتركين الجدد ويدعم تكوين replay و extraBufferCapacity و onBufferOverflow. وفقًا لجوجل (Android Developers, 2025)، SharedFlow هو الحل الموصى به لأوامر التنقل ورسائل Snackbar والأحداث الأخرى التي يجب معالجتها مرة واحدة فقط.

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

  • SharedFlow — تدفق ساخن للأحداث لمرة واحدة: المشتركون الجدد لا يتلقون القيم السابقة بدون تكوين replay.
  • MutableSharedFlow — إصدار قابل للتغيير بطرق emit() و tryEmit() لإرسال الأحداث.
  • SharedFlow vs StateFlow: SharedFlow لا يدمج القيم (يمكنه تخزين عدة قيم في المخزن المؤقت)، لا يتطلب قيمة أولية، مناسب للأحداث لمرة واحدة.
  • replay — عدد الأحداث الأخيرة المعاد تشغيلها للمشتركين الجدد (الافتراضي 0).
  • extraBufferCapacity — مخزن مؤقت إضافي للأحداث يتجاوز replay، لمنع حظر emit().

ما هو SharedFlow في Kotlin؟

SharedFlow هو تدفق ساخن (hot flow) من مكتبة kotlinx.coroutines.flow، والذي، على عكس StateFlow، غير مرتبط بحالة واحدة ويمكنه إصدار عدد تعسفي من الأحداث لمشتركين تعسفيين. SharedFlow هو النوع الأساسي لـ StateFlow — في الواقع StateFlow مُنفَّذ عبر SharedFlow مع replay = 1.

الميزة الرئيسية لـ SharedFlow هي أنه لا يجب عليه تخزين القيمة الأخيرة. بشكل افتراضي (replay = 0)، لا يتلقى المشترك الجديد شيئًا حتى يتم إرسال حدث جديد. هذا يجعل SharedFlow مثاليًا للسيناريوهات التي يجب فيها معالجة الحدث مرة واحدة فقط: التنقل، Snackbar، إشعارات النظام، نتائج مسح رمز QR.

تم تثبيت SharedFlow في kotlinx.coroutines 1.4.0 (نوفمبر 2020) مع StateFlow. وفقًا لتوثيق Kotlin Coroutines (2025)، يستخدم SharedFlow قفلًا دقيق الحبيبات لمزامنة المشتركين ويوفر قابلية توسع خطية تصل إلى 1000+ مشترك متزامن بدون تدهور في الأداء، وهو ما أكدته اختبارات JetBrains.

SharedFlow vs StateFlow: متى نستخدم كل منهما

الاختيار بين SharedFlow و StateFlow يعتمد على دلالات البيانات المنقولة: الحالة (StateFlow) أو الحدث (SharedFlow). فيما يلي معايير واضحة مع أمثلة.

المعيارSharedFlowStateFlow
الدلالةأحداث لمرة واحدة (تنقل، toast، تنبيه)حالة واجهة المستخدم (قائمة، تحميل، خطأ)
القيمة الأوليةغير مطلوبةمطلوبة
إعادة التشغيل عند الاشتراكفقط إذا كان replay > 0دائمًا القيمة الأخيرة
الدمجلا — الأحداث لا تُفقد (إذا لم يكن المخزن المؤقت ممتلئًا)نعم — يخزن فقط الأحدث
التخزين المؤقتقابل للتكوين عبر replay + extraBufferCapacityفقط 1 (replay=1 ثابت)
الاستخدامnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

أبسط قاعدة: إذا كان يجب عرض البيانات عند تدوير الشاشة — فهي حالة (StateFlow). إذا كان يجب ألا يتكرر الحدث عند تدوير الشاشة — فهو حدث لمرة واحدة (SharedFlow). على سبيل المثال، رسالة خطأ toast — SharedFlow: عند التدوير، لا يجب أن يظهر toast مرة أخرى. قائمة المنتجات — StateFlow: عند التدوير، يجب أن تبقى القائمة على الشاشة.

في IT Sectr نستخدم SharedFlow من أجل: أوامر التنقل (الانتقال بين الشاشات، فتح الروابط العميقة)، أحداث واجهة المستخدم (Snackbar، AlertDialog)، إشعارات النظام (تحديثات البيانات في الخلفية، نتيجة الدفع)، أحداث التحليلات (التسجيل، التتبع).

MutableSharedFlow: emit و tryEmit والتخزين المؤقت

MutableSharedFlow هو الإصدار القابل للتغيير من SharedFlow مع طريقتي emit() (suspend) و tryEmit() (غير suspend) لإرسال الأحداث. emit() تتعطل إذا كان المخزن المؤقت ممتلئًا و onBufferOverflow = SUSPEND. tryEmit() تُرجع Boolean يشير إلى ما إذا تمت إضافة الحدث بنجاح إلى المخزن المؤقت.

kotlin
class EventBus {
    private val _events = MutableSharedFlow<UiEvent>(
        replay = 0,
        extraBufferCapacity = 10,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val events: SharedFlow<UiEvent> get() = _events

    suspend fun sendEvent(event: UiEvent) {
        _events.emit(event)
    }

    fun trySendEvent(event: UiEvent): Boolean {
        return _events.tryEmit(event)
    }
}

sealed interface UiEvent {
    data class ShowSnackbar(val message: String) : UiEvent
    data class NavigateTo(val route: String) : UiEvent
    data class ShowDialog(val title: String, val message: String) : UiEvent
}

معلمات المُنشئ مهمة للغاية: replay = 0 يضمن أن الحدث لا يتكرر للمشترك الجديد؛ extraBufferCapacity = 10 يوفر مخزنًا مؤقتًا للإصدار السريع للأحداث قبل أن تشترك واجهة المستخدم؛ DROP_OLDEST هي استراتيجية الفائض: يتم التخلص من الأحداث القديمة، والحفاظ على الجديدة. وفقًا لـ Kotlin Coroutines Performance (JetBrains, 2024)، يعالج SharedFlow مع extraBufferCapacity = 64 أكثر من 100,000 حدث في الثانية دون فقدان.

SharedFlow للأحداث لمرة واحدة: نمط Event

نمط Event (أو UiEvent) هو الطريقة الموصى بها من جوجل لنقل الأحداث لمرة واحدة من ViewModel إلى View. على عكس الحالة (StateFlow)، يجب معالجة الحدث مرة واحدة فقط، وعند تدوير الشاشة لا يجب أن يتكرر. SharedFlow مع replay = 0 مثالي لهذه المهمة.

kotlin
class CheckoutViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
    val uiState: StateFlow<CheckoutState> get() = _uiState

    private val _event = MutableSharedFlow<CheckoutEvent>()
    val event: SharedFlow<CheckoutEvent> get() = _event

    fun placeOrder() {
        viewModelScope.launch {
            _uiState.value = CheckoutState.Loading
            try {
                val orderId = orderRepository.createOrder(cart)
                _uiState.value = CheckoutState.Success(orderId)
                _event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
            } catch (e: Exception) {
                _uiState.value = CheckoutState.Error(e.message)
                _event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "خطأ في التخطيط"))
            }
        }
    }
}

sealed interface CheckoutEvent {
    data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
    data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}

في View (Activity/Fragment): يجب أن يتم الاشتراك في الأحداث في lifecycleScope مع repeatOnLifecycle(STATE.STARTED). عند كل دخول إلى STARTED، يتم إعادة إنشاء الاشتراك، لكن الحدث لا يتكرر لأن SharedFlow مع replay=0 قد أطلقه بالفعل. هذا يضمن أن التنقل إلى شاشة تتبع الطلب يحدث مرة واحدة فقط، وليس في كل تدوير.

معلمات SharedFlow: replay و extraBufferCapacity و onBufferOverflow

مُنشئ MutableSharedFlow يقبل ثلاثة معلمات تحدد سلوك المخزن المؤقت. التكوين غير الصحيح يمكن أن يؤدي إلى فقدان الأحداث أو حظر emit().

المعلمةالنوعالافتراضيالوصف
replayInt0عدد الأحداث الأخيرة المعاد تشغيلها لمشترك جديد. 0 = لا يعيد التشغيل، 1 = مثل StateFlow
extraBufferCapacityInt0مخزن مؤقت إضافي يتجاوز replay. يتم تخزين الأحداث في مخزن مؤقت دائري. 64 هو الحد الموصى به لمعظم السيناريوهات
onBufferOverflowBufferOverflowSUSPENDاستراتيجية عند امتلاء المخزن المؤقت: SUSPEND، DROP_OLDEST، DROP_LATEST
kotlin
// تكوينات لسيناريوهات مختلفة:

// 1. أحداث واجهة المستخدم لمرة واحدة (تنقل، toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. تدفق إعادة التشغيل لمزامنة الحالة (مثل StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. إصدار أحداث عالي التردد (تحليلات، سجلات)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

مهم: extraBufferCapacity + replay = الحجم الإجمالي للمخزن المؤقت. إذا تم استدعاء emit() أسرع مما يعالج المشترك الأحداث، يمتلئ المخزن المؤقت ويتم تفعيل onBufferOverflow. لأحداث واجهة المستخدم، DROP_OLDEST هي استراتيجية آمنة: يتم التخلص من الأحداث القديمة (التنقلات غير ذات الصلة) لصالح الجديدة. للمعاملات المالية، استخدم SUSPEND — هذا يضمن عدم فقدان أي حدث على حساب حظر المرسل.

أمثلة كود: SharedFlow في Kotlin

مثال 1: SharedFlow للتنقل مع Jetpack Navigation

أوامر التنقل هي حالة استخدام كلاسيكية لـ SharedFlow. Fragment يشترك في الأحداث وينفذ التنقل. عند تدوير الشاشة، لا يتكرر الأمر.

kotlin
// ViewModel
class AuthViewModel : ViewModel() {
    private val _navEvent = MutableSharedFlow<NavEvent>()
    val navEvent: SharedFlow<NavEvent> get() = _navEvent

    fun onLoginSuccess() {
        viewModelScope.launch {
            _navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
        }
    }

    fun onLogout() {
        viewModelScope.launch {
            _navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
        }
    }
}

sealed interface NavEvent {
    data class NavigateTo(val route: String) : NavEvent
    data class NavigateBack(val popUpTo: String? = null) : NavEvent
}

// في Fragment:
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.navEvent.collect { navEvent ->
            when (navEvent) {
                is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
                is NavEvent.NavigateBack -> findNavController().popBackStack()
            }
        }
    }
}

مثال 2: SharedFlow مع Room وعوامل Flow

سيناريو معقد: SharedFlow لإشعارات الأحداث في الخلفية مع StateFlow لواجهة المستخدم.

kotlin
class NotificationViewModel : ViewModel() {
    private val _toastMessage = MutableSharedFlow<String>()
    val toastMessage: SharedFlow<String> get() = _toastMessage

    private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
    val notifications: StateFlow<List<Notification>> get() = _notifications

    init {
        viewModelScope.launch {
            notificationChannel
                .consumeAsFlow()
                .collect { notification ->
                    _notifications.value = _notifications.value + notification
                    _toastMessage.emit("إشعار جديد: ${notification.title}")
                }
        }
    }

    fun dismissNotification(id: String) {
        _notifications.value = _notifications.value.filter { it.id != id }
    }

    fun markAllRead() {
        viewModelScope.launch {
            _notifications.value = _notifications.value.map { it.copy(isRead = true) }
            _toastMessage.emit("تم وضع علامة مقروءة على جميع الإشعارات")
        }
    }
}

في هذا المثال: StateFlow يخزن قائمة الإشعارات (الحالة — تُحفظ عند التدوير)، SharedFlow يصدر رسائل toast (أحداث لمرة واحدة — لا تتكرر عند التدوير). الجمع بين نوعي Flow هو النمط الموصى به من جوجل لـ ViewModel بدءًا من 2022.

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

هل يمكن لـ SharedFlow فقدان حدث؟

نعم، إذا كان المخزن المؤقت ممتلئًا و onBufferOverflow = DROP_OLDEST أو DROP_LATEST. SharedFlow لا يضمن توصيل كل حدث — إنه ليس قائمة انتظار رسائل (مثل Channel). إذا كنت بحاجة إلى توصيل مضمون لجميع الأحداث، استخدم Channel بمخزن مؤقت غير محدود (UNLIMITED) أو BroadcastChannel (مهمل). لأحداث واجهة المستخدم، فقدان الأحداث القديمة (مثل التنقل السابق) هو سلوك متوقع، وليس خطأ.

كيف يختلف SharedFlow عن Channel؟

Channel هو قائمة انتظار FIFO حيث يتم توصيل كل حدث إلى مشترك واحد بالضبط (نقطة إلى نقطة). SharedFlow هو بث: يتم توصيل كل حدث إلى جميع المشتركين النشطين. SharedFlow أقرب إلى BroadcastChannel (المهمل) ومناسب لسيناريوهات واحد إلى متعدد. Channel مخصص لواحد إلى واحد (مجموعات الخيوط، خطوط الأنابيب). وفقًا لتوصية JetBrains، SharedFlow هو بديل BroadcastChannel في جميع المشاريع الجديدة.

كيف أجعل SharedFlow آمنًا للخيوط؟

SharedFlow آمن للخيوط بالفعل — emit() و collect() متزامنتان بشكل صحيح. يمكن لعدة خيوط استدعاء emit() بدون أقفال، وجميع المشتركين النشطين يتلقون الأحداث بالترتيب الصحيح. tryEmit() غير محظورة — تُرجع false إذا كان المخزن المؤقت ممتلئًا. للأنظمة عالية الحمل، استخدم tryEmit() مع DROP_OLDEST — هذا يمنع حظر الخيوط.

لماذا لا يُستخدم SharedFlow للحالة؟

SharedFlow بدون replay=1 لا يخزن القيمة الأخيرة — عند تدوير الشاشة، لن يتلقى المشترك الجديد الحالة الحالية وستبقى واجهة المستخدم فارغة. مع replay=1، يتصرف SharedFlow مثل StateFlow لكنه يفقد تحسين المقارنة عبر equals()، مما يسبب إشعارات غير ضرورية عند إصدار نفس القيمة مرة أخرى. StateFlow هو الخيار الصحيح للحالة؛ SharedFlow للأحداث.

كيف أختبر SharedFlow؟

لاختبار SharedFlow، استخدم Turbine — مكتبة Kotlin لاختبار Flow. تسمح Turbine بالتحقق من كل إصدار على حدة مع مهلات والتحقق من الاكتمال. مثال: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. يمكنك أيضًا استخدام .toList() في runTest مع تحديد عدد الأحداث المتوقعة.

الخلاصة

  • SharedFlow — تدفق تفاعلي ساخن للأحداث لمرة واحدة غير مرتبطة بالحالة الأخيرة.
  • SharedFlow vs StateFlow: SharedFlow للأحداث (تنقل، toasts، تنبيهات)، StateFlow للحالة (قوائم، تحميل، أخطاء).
  • MutableSharedFlow مع replay=0، extraBufferCapacity=5، DROP_OLDEST هو التكوين القياسي لأحداث واجهة المستخدم.
  • emit() — دالة suspend للإصدار المحظور؛ tryEmit() — غير suspend بنتيجة Boolean.
  • نمط UiEvent مع sealed class هو الطريقة الموصى بها من جوجل لنقل الأحداث لمرة واحدة من ViewModel إلى View.
  • SharedFlow يضمن التوصيل لكل مشترك، لكنه لا يضمن توصيل كل حدث عند فائض المخزن المؤقت.
  • الجمع بين SharedFlow + StateFlow في ViewModel واحد هو النمط الأمثل لبنية تفصل بين الحالة والأحداث.

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

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

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

اقرأ أيضًا