SharedFlow هو تدفق تفاعلي ساخن من مكتبة Kotlin Coroutines، مُحسَّن للأحداث لمرة واحدة (one-shot events) التي لا يجب أن تتكرر عند تدوير الشاشة أو إعادة إنشاء المشترك. نعرض كيف يختلف SharedFlow عن StateFlow: على عكس StateFlow، لا يخزن SharedFlow القيمة الأخيرة للمشتركين الجدد ويدعم تكوين replay و extraBufferCapacity و onBufferOverflow. وفقًا لجوجل (Android Developers, 2025)، SharedFlow هو الحل الموصى به لأوامر التنقل ورسائل Snackbar والأحداث الأخرى التي يجب معالجتها مرة واحدة فقط.
النقاط الرئيسية
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 و StateFlow يعتمد على دلالات البيانات المنقولة: الحالة (StateFlow) أو الحدث (SharedFlow). فيما يلي معايير واضحة مع أمثلة.
| المعيار | SharedFlow | StateFlow |
|---|---|---|
| الدلالة | أحداث لمرة واحدة (تنقل، toast، تنبيه) | حالة واجهة المستخدم (قائمة، تحميل، خطأ) |
| القيمة الأولية | غير مطلوبة | مطلوبة |
| إعادة التشغيل عند الاشتراك | فقط إذا كان replay > 0 | دائمًا القيمة الأخيرة |
| الدمج | لا — الأحداث لا تُفقد (إذا لم يكن المخزن المؤقت ممتلئًا) | نعم — يخزن فقط الأحدث |
| التخزين المؤقت | قابل للتكوين عبر replay + extraBufferCapacity | فقط 1 (replay=1 ثابت) |
| الاستخدام | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
أبسط قاعدة: إذا كان يجب عرض البيانات عند تدوير الشاشة — فهي حالة (StateFlow). إذا كان يجب ألا يتكرر الحدث عند تدوير الشاشة — فهو حدث لمرة واحدة (SharedFlow). على سبيل المثال، رسالة خطأ toast — SharedFlow: عند التدوير، لا يجب أن يظهر toast مرة أخرى. قائمة المنتجات — StateFlow: عند التدوير، يجب أن تبقى القائمة على الشاشة.
في IT Sectr نستخدم SharedFlow من أجل: أوامر التنقل (الانتقال بين الشاشات، فتح الروابط العميقة)، أحداث واجهة المستخدم (Snackbar، AlertDialog)، إشعارات النظام (تحديثات البيانات في الخلفية، نتيجة الدفع)، أحداث التحليلات (التسجيل، التتبع).
MutableSharedFlow هو الإصدار القابل للتغيير من SharedFlow مع طريقتي emit() (suspend) و tryEmit() (غير suspend) لإرسال الأحداث. emit() تتعطل إذا كان المخزن المؤقت ممتلئًا و onBufferOverflow = SUSPEND. tryEmit() تُرجع Boolean يشير إلى ما إذا تمت إضافة الحدث بنجاح إلى المخزن المؤقت.
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 حدث في الثانية دون فقدان.
نمط Event (أو UiEvent) هو الطريقة الموصى بها من جوجل لنقل الأحداث لمرة واحدة من ViewModel إلى View. على عكس الحالة (StateFlow)، يجب معالجة الحدث مرة واحدة فقط، وعند تدوير الشاشة لا يجب أن يتكرر. SharedFlow مع replay = 0 مثالي لهذه المهمة.
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 قد أطلقه بالفعل. هذا يضمن أن التنقل إلى شاشة تتبع الطلب يحدث مرة واحدة فقط، وليس في كل تدوير.
مُنشئ MutableSharedFlow يقبل ثلاثة معلمات تحدد سلوك المخزن المؤقت. التكوين غير الصحيح يمكن أن يؤدي إلى فقدان الأحداث أو حظر emit().
| المعلمة | النوع | الافتراضي | الوصف |
|---|---|---|---|
| replay | Int | 0 | عدد الأحداث الأخيرة المعاد تشغيلها لمشترك جديد. 0 = لا يعيد التشغيل، 1 = مثل StateFlow |
| extraBufferCapacity | Int | 0 | مخزن مؤقت إضافي يتجاوز replay. يتم تخزين الأحداث في مخزن مؤقت دائري. 64 هو الحد الموصى به لمعظم السيناريوهات |
| onBufferOverflow | BufferOverflow | SUSPEND | استراتيجية عند امتلاء المخزن المؤقت: SUSPEND، DROP_OLDEST، DROP_LATEST |
// تكوينات لسيناريوهات مختلفة:
// 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. Fragment يشترك في الأحداث وينفذ التنقل. عند تدوير الشاشة، لا يتكرر الأمر.
// 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()
}
}
}
}
سيناريو معقد: SharedFlow لإشعارات الأحداث في الخلفية مع StateFlow لواجهة المستخدم.
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.
الأسئلة الشائعة
نعم، إذا كان المخزن المؤقت ممتلئًا و onBufferOverflow = DROP_OLDEST أو DROP_LATEST. SharedFlow لا يضمن توصيل كل حدث — إنه ليس قائمة انتظار رسائل (مثل Channel). إذا كنت بحاجة إلى توصيل مضمون لجميع الأحداث، استخدم Channel بمخزن مؤقت غير محدود (UNLIMITED) أو BroadcastChannel (مهمل). لأحداث واجهة المستخدم، فقدان الأحداث القديمة (مثل التنقل السابق) هو سلوك متوقع، وليس خطأ.
Channel هو قائمة انتظار FIFO حيث يتم توصيل كل حدث إلى مشترك واحد بالضبط (نقطة إلى نقطة). SharedFlow هو بث: يتم توصيل كل حدث إلى جميع المشتركين النشطين. SharedFlow أقرب إلى BroadcastChannel (المهمل) ومناسب لسيناريوهات واحد إلى متعدد. Channel مخصص لواحد إلى واحد (مجموعات الخيوط، خطوط الأنابيب). وفقًا لتوصية JetBrains، SharedFlow هو بديل BroadcastChannel في جميع المشاريع الجديدة.
SharedFlow آمن للخيوط بالفعل — emit() و collect() متزامنتان بشكل صحيح. يمكن لعدة خيوط استدعاء emit() بدون أقفال، وجميع المشتركين النشطين يتلقون الأحداث بالترتيب الصحيح. tryEmit() غير محظورة — تُرجع false إذا كان المخزن المؤقت ممتلئًا. للأنظمة عالية الحمل، استخدم tryEmit() مع DROP_OLDEST — هذا يمنع حظر الخيوط.
SharedFlow بدون replay=1 لا يخزن القيمة الأخيرة — عند تدوير الشاشة، لن يتلقى المشترك الجديد الحالة الحالية وستبقى واجهة المستخدم فارغة. مع replay=1، يتصرف SharedFlow مثل StateFlow لكنه يفقد تحسين المقارنة عبر equals()، مما يسبب إشعارات غير ضرورية عند إصدار نفس القيمة مرة أخرى. StateFlow هو الخيار الصحيح للحالة؛ SharedFlow للأحداث.
لاختبار SharedFlow، استخدم Turbine — مكتبة Kotlin لاختبار Flow. تسمح Turbine بالتحقق من كل إصدار على حدة مع مهلات والتحقق من الاكتمال. مثال: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. يمكنك أيضًا استخدام .toList() في runTest مع تحديد عدد الأحداث المتوقعة.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا