SharedFlow: چیست، SharedFlow vs StateFlow در Android

نویسنده: IT Sectr منتشر شده: 2026-02-20 زمان مطالعه: 9 دقیقه

SharedFlow — یک جریان واکنشی داغ از کتابخانه Kotlin Coroutines، بهینه‌سازی‌شده برای رویدادهای یک‌باره (one-shot events) که نباید هنگام چرخش صفحه یا بازآفرینی مشترک تکرار شوند. نشان می‌دهیم که SharedFlow چه تفاوتی با StateFlow دارد: برخلاف StateFlow، SharedFlow آخرین مقدار را برای مشترکین جدید ذخیره نمی‌کند و از تنظیمات replay، extraBufferCapacity و onBufferOverflow پشتیبانی می‌کند. به گفته Google (Android Developers, 2025)، SharedFlow راه‌حل توصیه‌شده برای دستورات ناوبری، پیام‌های Snackbar و سایر رویدادهایی است که باید دقیقاً یک بار پردازش شوند.

نکات اصلی

  • SharedFlow — hot flow برای رویدادهای یک‌باره: مشترکین جدید بدون تنظیم 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 همراه با StateFlow در kotlinx.coroutines 1.4.0 (نوامبر 2020) پایدار شد. طبق مستندات Kotlin Coroutines (2025)، SharedFlow از قفل دانه‌ریز برای همگام‌سازی مشترکین استفاده می‌کند و مقیاس‌پذیری خطی تا 1000+ مشترک همزمان بدون کاهش عملکرد را تضمین می‌کند که توسط تست‌های JetBrains تأیید شده است.

SharedFlow vs StateFlow: چه زمانی از کدام استفاده کنیم

انتخاب بین SharedFlow و StateFlow به معناشناسی داده‌های منتقل‌شده بستگی دارد: حالت (StateFlow) یا رویداد (SharedFlow). در زیر معیارهای واضح با مثال‌ها آورده شده است.

معیارSharedFlowStateFlow
معناشناسیرویدادهای یک‌باره (ناوبری، توست، هشدار)وضعیت UI (لیست، بارگذاری، خطا)
مقدار اولیهنیاز نیستاجباری
تکرار در اشتراکفقط اگر replay > 0همیشه آخرین مقدار
ادغام (Confiation)خیر — رویدادها از دست نمی‌روند (اگر بافر پر نشود)بله — فقط آخرین را ذخیره می‌کند
بافرینگقابل تنظیم از طریق replay + extraBufferCapacityفقط 1 (replay=1 ثابت)
کاربردnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

ساده‌ترین قانون: اگر داده‌ها باید هنگام چرخش صفحه نمایش داده شوند — این حالت است (StateFlow). اگر هنگام چرخش صفحه رویداد نباید تکرار شود — این رویداد یک‌باره است (SharedFlow). مثلاً «توست با پیام خطا» — SharedFlow: هنگام چرخش توست نباید دوباره نمایش داده شود. «لیست محصولات» — StateFlow: هنگام چرخش لیست باید روی صفحه باقی بماند.

در IT Sectr از SharedFlow برای: دستورات ناوبری (رفتن به صفحه، باز کردن دیپ‌لینک)، رویدادهای UI (Snackbar، AlertDialog)، اعلان‌های سیستمی (به‌روزرسانی داده در پس‌زمینه، نتیجه پرداخت)، رویدادهای تحلیلی (لاگ‌گیری، ردیابی) استفاده می‌کنیم.

MutableSharedFlow: emit، tryEmit و بافرینگ

MutableSharedFlow — نسخه قابل تغییر SharedFlow با متدهای emit() (suspend) و tryEmit() (non-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 — بافر برای ارسال سریع رویدادها قبل از اشتراک UI؛ DROP_OLDEST — استراتژی سرریز: رویدادهای قدیمی حذف می‌شوند، جدیدها حفظ می‌شوند. طبق Kotlin Coroutines Performance (JetBrains, 2024)، SharedFlow با extraBufferCapacity = 64 بیش از 100٬000 رویداد در ثانیه را بدون افت پردازش می‌کند.

SharedFlow برای رویدادهای یک‌باره: الگوی Event

الگوی Event (یا UiEvent) — روش توصیه‌شده توسط Google برای انتقال رویدادهای یک‌باره از 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. رویدادهای یک‌باره UI (ناوبری، توست‌ها)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. جریان replay برای همگام‌سازی حالت (مانند 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 فعال می‌شود. برای رویدادهای UI، 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 برای UI.

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 پیام‌های توست را ارسال می‌کند (رویدادهای یک‌باره — در چرخش تکرار نمی‌شوند). ترکیب دو نوع Flow — الگوی توصیه‌شده Google برای ViewModel از سال 2022.

سوالات متداول

آیا SharedFlow می‌تواند رویدادی را از دست بدهد؟

بله، اگر بافر پر شده باشد و onBufferOverflow = DROP_OLDEST یا DROP_LATEST باشد. SharedFlow تحویل هر رویداد را تضمین نمی‌کند — این یک صف پیام نیست (مانند Channel). اگر تحویل تضمینی همه رویدادها لازم است، از Channel با بافر غیرقابل سرریز (UNLIMITED) یا BroadcastChannel (deprecated) استفاده کنید. برای رویدادهای UI، از دست رفتن رویدادهای قدیمی (مثلاً ناوبری قدیمی) — رفتار مورد انتظار است، نه یک باگ.

تفاوت SharedFlow با Channel چیست؟

Channel — یک صف FIFO است که در آن هر رویداد دقیقاً به یک مشترک تحویل داده می‌شود (نقطه-به-نقطه). SharedFlow — پخش: هر رویداد به تمام مشترکین فعال تحویل داده می‌شود. SharedFlow به BroadcastChannel (که deprecated است) نزدیک‌تر است و برای سناریوهای «یک-به-چند» مناسب است. Channel — برای «یک-به-یک» (استخرهای نخ، pipeline). طبق توصیه JetBrains، SharedFlow جایگزین BroadcastChannel برای همه پروژه‌های جدید است.

چگونه SharedFlow را thread-safe کنیم؟

SharedFlow از قبل thread-safe است — emit() و collect() به درستی همگام‌سازی شده‌اند. چندین نخ می‌توانند بدون قفل emit() را فراخوانی کنند و همه مشترکین فعال رویدادها را به ترتیب درست دریافت می‌کنند. tryEmit() غیرمسدودکننده است — اگر بافر پر باشد false برمی‌گرداند. برای سیستم‌های پربار از tryEmit() با DROP_OLDEST استفاده کنید — این کار از مسدود شدن نخ‌ها جلوگیری می‌کند.

چرا SharedFlow برای حالت استفاده نمی‌شود؟

SharedFlow بدون replay=1 آخرین مقدار را ذخیره نمی‌کند — هنگام چرخش صفحه مشترک جدید حالت فعلی را دریافت نمی‌کند، UI خالی می‌ماند. با 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 — رویدادها (ناوبری، توست‌ها، هشدارها)، StateFlow — حالت (لیست‌ها، بارگذاری، خطاها).
  • MutableSharedFlow با replay=0، extraBufferCapacity=5، DROP_OLDEST — تنظیمات استاندارد برای رویدادهای UI.
  • emit() — تابع suspend برای ارسال مسدودکننده؛ tryEmit() — non-suspend با نتیجه Boolean.
  • الگوی UiEvent با sealed class — روش توصیه‌شده برای انتقال رویدادهای یک‌باره از ViewModel به View.
  • SharedFlow تحویل به هر مشترک را تضمین می‌کند، اما تحویل هر رویداد را هنگام سرریز بافر تضمین نمی‌کند.
  • ترکیب SharedFlow + StateFlow در یک ViewModel — الگوی بهینه برای معماری جداکننده حالت و رویدادها.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید