SharedFlow — یک جریان واکنشی داغ از کتابخانه Kotlin Coroutines، بهینهسازیشده برای رویدادهای یکباره (one-shot events) که نباید هنگام چرخش صفحه یا بازآفرینی مشترک تکرار شوند. نشان میدهیم که SharedFlow چه تفاوتی با StateFlow دارد: برخلاف StateFlow، SharedFlow آخرین مقدار را برای مشترکین جدید ذخیره نمیکند و از تنظیمات replay، extraBufferCapacity و onBufferOverflow پشتیبانی میکند. به گفته Google (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 همراه با StateFlow در kotlinx.coroutines 1.4.0 (نوامبر 2020) پایدار شد. طبق مستندات Kotlin Coroutines (2025)، SharedFlow از قفل دانهریز برای همگامسازی مشترکین استفاده میکند و مقیاسپذیری خطی تا 1000+ مشترک همزمان بدون کاهش عملکرد را تضمین میکند که توسط تستهای JetBrains تأیید شده است.
انتخاب بین SharedFlow و StateFlow به معناشناسی دادههای منتقلشده بستگی دارد: حالت (StateFlow) یا رویداد (SharedFlow). در زیر معیارهای واضح با مثالها آورده شده است.
| معیار | SharedFlow | StateFlow |
|---|---|---|
| معناشناسی | رویدادهای یکباره (ناوبری، توست، هشدار) | وضعیت UI (لیست، بارگذاری، خطا) |
| مقدار اولیه | نیاز نیست | اجباری |
| تکرار در اشتراک | فقط اگر replay > 0 | همیشه آخرین مقدار |
| ادغام (Confiation) | خیر — رویدادها از دست نمیروند (اگر بافر پر نشود) | بله — فقط آخرین را ذخیره میکند |
| بافرینگ | قابل تنظیم از طریق replay + extraBufferCapacity | فقط 1 (replay=1 ثابت) |
| کاربرد | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
سادهترین قانون: اگر دادهها باید هنگام چرخش صفحه نمایش داده شوند — این حالت است (StateFlow). اگر هنگام چرخش صفحه رویداد نباید تکرار شود — این رویداد یکباره است (SharedFlow). مثلاً «توست با پیام خطا» — SharedFlow: هنگام چرخش توست نباید دوباره نمایش داده شود. «لیست محصولات» — StateFlow: هنگام چرخش لیست باید روی صفحه باقی بماند.
در IT Sectr از SharedFlow برای: دستورات ناوبری (رفتن به صفحه، باز کردن دیپلینک)، رویدادهای UI (Snackbar، AlertDialog)، اعلانهای سیستمی (بهروزرسانی داده در پسزمینه، نتیجه پرداخت)، رویدادهای تحلیلی (لاگگیری، ردیابی) استفاده میکنیم.
MutableSharedFlow — نسخه قابل تغییر SharedFlow با متدهای emit() (suspend) و tryEmit() (non-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 — بافر برای ارسال سریع رویدادها قبل از اشتراک UI؛ DROP_OLDEST — استراتژی سرریز: رویدادهای قدیمی حذف میشوند، جدیدها حفظ میشوند. طبق Kotlin Coroutines Performance (JetBrains, 2024)، SharedFlow با extraBufferCapacity = 64 بیش از 100٬000 رویداد در ثانیه را بدون افت پردازش میکند.
الگوی Event (یا UiEvent) — روش توصیهشده توسط Google برای انتقال رویدادهای یکباره از 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. رویدادهای یکباره 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. 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 برای UI.
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.
سوالات متداول
بله، اگر بافر پر شده باشد و onBufferOverflow = DROP_OLDEST یا DROP_LATEST باشد. SharedFlow تحویل هر رویداد را تضمین نمیکند — این یک صف پیام نیست (مانند Channel). اگر تحویل تضمینی همه رویدادها لازم است، از Channel با بافر غیرقابل سرریز (UNLIMITED) یا BroadcastChannel (deprecated) استفاده کنید. برای رویدادهای UI، از دست رفتن رویدادهای قدیمی (مثلاً ناوبری قدیمی) — رفتار مورد انتظار است، نه یک باگ.
Channel — یک صف FIFO است که در آن هر رویداد دقیقاً به یک مشترک تحویل داده میشود (نقطه-به-نقطه). SharedFlow — پخش: هر رویداد به تمام مشترکین فعال تحویل داده میشود. SharedFlow به BroadcastChannel (که deprecated است) نزدیکتر است و برای سناریوهای «یک-به-چند» مناسب است. Channel — برای «یک-به-یک» (استخرهای نخ، pipeline). طبق توصیه JetBrains، SharedFlow جایگزین BroadcastChannel برای همه پروژههای جدید است.
SharedFlow از قبل thread-safe است — emit() و collect() به درستی همگامسازی شدهاند. چندین نخ میتوانند بدون قفل emit() را فراخوانی کنند و همه مشترکین فعال رویدادها را به ترتیب درست دریافت میکنند. tryEmit() غیرمسدودکننده است — اگر بافر پر باشد false برمیگرداند. برای سیستمهای پربار از tryEmit() با DROP_OLDEST استفاده کنید — این کار از مسدود شدن نخها جلوگیری میکند.
SharedFlow بدون replay=1 آخرین مقدار را ذخیره نمیکند — هنگام چرخش صفحه مشترک جدید حالت فعلی را دریافت نمیکند، UI خالی میماند. با replay=1 SharedFlow مانند StateFlow رفتار میکند اما بهینهسازی مقایسه از طریق equals() را از دست میدهد که باعث اعلانهای اضافی هنگام ارسال مجدد همان مقدار میشود. StateFlow — انتخاب درست برای حالت؛ SharedFlow — برای رویدادها.
برای تست SharedFlow از Turbine — کتابخانه Kotlin برای تست Flow استفاده کنید. Turbine به شما امکان میدهد هر انتشار را جداگانه با تایماوت و بررسی اتمام بررسی کنید. مثال: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. همچنین میتوانید از .toList() در runTest با تعیین تعداد رویدادهای مورد انتظار استفاده کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید