SharedFlow হল Kotlin Coroutines লাইব্রেরির একটি হট রিঅ্যাকটিভ ফ্লো, যা একবার ঘটে এমন ঘটনার (one-shot events) জন্য অপ্টিমাইজ করা হয়েছে যা স্ক্রিন রোটেশন বা সাবস্ক্রাইবার পুনঃনির্মাণে পুনরাবৃত্তি হওয়া উচিত নয়। আমরা দেখাই কিভাবে SharedFlow StateFlow থেকে আলাদা: StateFlow-এর বিপরীতে, SharedFlow নতুন সাবস্ক্রাইবারদের জন্য শেষ মান সংরক্ষণ করে না এবং replay, extraBufferCapacity এবং onBufferOverflow কনফিগারেশন সমর্থন করে। Google (Android Developers, 2025) অনুসারে, SharedFlow হল নেভিগেশন কমান্ড, Snackbar বার্তা এবং অন্যান্য ইভেন্টের জন্য প্রস্তাবিত সমাধান যা ঠিক একবার প্রক্রিয়াজাত করা উচিত।
মূল পয়েন্ট
SharedFlow হল kotlinx.coroutines.flow লাইব্রেরির একটি হট ফ্লো (hot flow), যা StateFlow-এর বিপরীতে, একটি একক অবস্থার সাথে আবদ্ধ নয় এবং ইচ্ছামত সাবস্ক্রাইবারদের কাছে ইচ্ছামত সংখ্যক ইভেন্ট নির্গত করতে পারে। SharedFlow হল StateFlow-এর জন্য বেস টাইপ — StateFlow আসলে replay = 1 সহ SharedFlow-এর মাধ্যমে বাস্তবায়িত।
SharedFlow-এর মূল বৈশিষ্ট্য হল এটিকে শেষ মান সংরক্ষণ করতে হবে না। ডিফল্টভাবে (replay = 0), একটি নতুন সাবস্ক্রাইবার কিছুই পায় না যতক্ষণ না একটি নতুন ইভেন্ট পাঠানো হয়। এটি SharedFlow-কে সেই পরিস্থিতির জন্য আদর্শ করে তোলে যেখানে একটি ইভেন্ট ঠিক একবার প্রক্রিয়াজাত করা উচিত: নেভিগেশন, Snackbar, সিস্টেম নোটিফিকেশন, QR কোড স্ক্যান ফলাফল।
SharedFlow kotlinx.coroutines 1.4.0 (নভেম্বর 2020) এ StateFlow-এর সাথে স্থিতিশীল করা হয়েছিল। Kotlin Coroutines ডকুমেন্টেশন (2025) অনুসারে, SharedFlow সাবস্ক্রাইবার সিঙ্ক্রোনাইজেশনের জন্য ফাইন-গ্রেইনড লকিং ব্যবহার করে এবং JetBrains পরীক্ষা দ্বারা নিশ্চিতকৃত কর্মক্ষমতা হ্রাস ছাড়াই 1000+ সমবর্তী সাবস্ক্রাইবার পর্যন্ত রৈখিক মাপযোগ্যতা প্রদান করে।
SharedFlow এবং StateFlow-এর মধ্যে পছন্দ স্থানান্তরিত ডেটার শব্দার্থের উপর নির্ভর করে: অবস্থা (StateFlow) বা ইভেন্ট (SharedFlow)। নীচে উদাহরণ সহ স্পষ্ট মানদণ্ড দেওয়া হল।
| মানদণ্ড | SharedFlow | StateFlow |
|---|---|---|
| শব্দার্থ | একবার ঘটে এমন ঘটনা (নেভিগেশন, টোস্ট, সতর্কতা) | UI অবস্থা (তালিকা, লোডিং, ত্রুটি) |
| প্রাথমিক মান | প্রয়োজন নেই | প্রয়োজন |
| সাবস্ক্রিপশনে পুনরায় চালানো | শুধুমাত্র যদি replay > 0 | সর্বদা শেষ মান |
| একত্রীকরণ | না — ইভেন্ট হারায় না (যদি বাফার পূর্ণ না হয়) | হ্যাঁ — শুধুমাত্র সর্বশেষ সংরক্ষণ করে |
| বাফারিং | 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() (নন-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) অনুসারে, extraBufferCapacity = 64 সহ SharedFlow ক্ষতি ছাড়াই প্রতি সেকেন্ডে 100,000-এর বেশি ইভেন্ট প্রক্রিয়া করে।
ইভেন্ট প্যাটার্ন (বা UiEvent) হল ViewModel থেকে View-এ একবার ঘটে এমন ইভেন্ট পাঠানোর Google-এর প্রস্তাবিত উপায়। অবস্থার (StateFlow) বিপরীতে, একটি ইভেন্ট ঠিক একবার প্রক্রিয়াজাত করা উচিত এবং স্ক্রিন রোটেশনে এটি পুনরাবৃত্তি হওয়া উচিত নয়। replay = 0 সহ SharedFlow এই কাজের জন্য আদর্শ।
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-এ প্রতিটি প্রবেশে, সাবস্ক্রিপশন পুনরায় তৈরি হয়, কিন্তু ইভেন্টটি পুনরাবৃত্তি হয় না কারণ replay=0 সহ SharedFlow এটি ইতিমধ্যেই মুক্তি দিয়েছে। এটি নিশ্চিত করে যে অর্ডার ট্র্যাকিং স্ক্রিনে নেভিগেশন শুধুমাত্র একবার ঘটে, প্রতিটি রোটেশনে নয়।
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. অবস্থা সিঙ্ক্রোনাইজেশনের জন্য রিপ্লে ফ্লো (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()
}
}
}
}
একটি জটিল পরিস্থিতি: UI-এর জন্য StateFlow-এর সাথে মিলিত ব্যাকগ্রাউন্ড ইভেন্ট বিজ্ঞপ্তিগুলির জন্য SharedFlow।
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 প্রকারের সংমিশ্রণ হল 2022 থেকে ViewModel-এর জন্য Google-এর প্রস্তাবিত প্যাটার্ন।
সচরাচর জিজ্ঞাসিত প্রশ্ন
হ্যাঁ, যদি বাফার পূর্ণ থাকে এবং onBufferOverflow = DROP_OLDEST বা DROP_LATEST হয়। SharedFlow প্রতিটি ইভেন্টের ডেলিভারির গ্যারান্টি দেয় না — এটি একটি বার্তা সারি (Channel-এর মতো) নয়। যদি আপনার সকল ইভেন্টের গ্যারান্টিযুক্ত ডেলিভারির প্রয়োজন হয়, তাহলে সীমাহীন বাফার (UNLIMITED) বা BroadcastChannel (অপ্রচলিত) সহ Channel ব্যবহার করুন। UI ইভেন্টের জন্য, পুরানো ইভেন্ট হারানো (যেমন, পুরানো নেভিগেশন) প্রত্যাশিত আচরণ, বাগ নয়।
Channel হল একটি FIFO সারি যেখানে প্রতিটি ইভেন্ট ঠিক একজন সাবস্ক্রাইবারকে সরবরাহ করা হয় (পয়েন্ট-টু-পয়েন্ট)। SharedFlow হল একটি সম্প্রচার: প্রতিটি ইভেন্ট সমস্ত সক্রিয় সাবস্ক্রাইবারকে সরবরাহ করা হয়। SharedFlow BroadcastChannel-এর (যা অপ্রচলিত) কাছাকাছি এবং এক-থেকে-অনেক পরিস্থিতির জন্য উপযুক্ত। Channel এক-থেকে-এক (থ্রেড পুল, পাইপলাইন) এর জন্য। JetBrains-এর সুপারিশ অনুসারে, SharedFlow হল সমস্ত নতুন প্রকল্পে BroadcastChannel-এর প্রতিস্থাপন।
SharedFlow ইতিমধ্যেই থ্রেড-সেফ — emit() এবং collect() সঠিকভাবে সিঙ্ক্রোনাইজ করা আছে। একাধিক থ্রেড লক ছাড়াই emit() কল করতে পারে এবং সমস্ত সক্রিয় সাবস্ক্রাইবার সঠিক ক্রমে ইভেন্ট পায়। tryEmit() নন-ব্লকিং — বাফার পূর্ণ হলে false ফেরত দেয়। উচ্চ-লোড সিস্টেমের জন্য, DROP_OLDEST সহ tryEmit() ব্যবহার করুন — এটি থ্রেড ব্লকিং প্রতিরোধ করে।
replay=1 ছাড়া SharedFlow শেষ মান সংরক্ষণ করে না — স্ক্রিন রোটেশনে একটি নতুন সাবস্ক্রাইবার বর্তমান অবস্থা পাবে না এবং UI খালি থাকবে। replay=1 সহ, SharedFlow StateFlow-এর মতো আচরণ করে কিন্তু equals() এর মাধ্যমে তুলনা অপ্টিমাইজেশন হারায়, যার ফলে একই মান পুনরায় নির্গত হলে অপ্রয়োজনীয় বিজ্ঞপ্তি হয়। StateFlow অবস্থার জন্য সঠিক পছন্দ; SharedFlow ইভেন্টের জন্য।
SharedFlow পরীক্ষার জন্য, Turbine ব্যবহার করুন — Flow পরীক্ষার জন্য একটি Kotlin লাইব্রেরি। Turbine টাইমআউট এবং সমাপ্তি যাচাই সহ প্রতিটি নির্গমন পৃথকভাবে পরীক্ষা করার অনুমতি দেয়। উদাহরণ: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }। আপনি প্রত্যাশিত ইভেন্টের সংখ্যা উল্লেখ করে runTest-এ .toList() ব্যবহার করতে পারেন।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন