Jetpack Compose में StateFlow बनाम SharedFlow: रीप्ले बग के बिना UI स्टेट मॉडल करना
एक Compose ViewModel में StateFlow कब इस्तेमाल करें और SharedFlow कब — और दोनों को मिला देने पर सामने आने वाला वन-टाइम-इवेंट बग।
जो भी Compose स्क्रीन मैंने अब तक शिप की है, वह अंत में एक ही सवाल पर आ टिकती है: क्या यह जानकारी बताती है कि स्क्रीन इस वक़्त कैसी दिखती है, या यह बताती है कि एक बार कुछ हुआ था? जवाब गलत मिले तो एक खास, परेशान करने वाली बग सामने आती है — रोटेशन के बाद फिर से दिखने वाला टोस्ट, दो बार फायर होने वाला नेविगेशन इवेंट, या “सेशन पूरा हुआ” वाला सेलिब्रेशन जो हर बार उपयोगकर्ता ऐप को बैकग्राउंड में भेजकर फिर से खोलने पर दोबारा चलता है। इसका हल कोई वर्कअराउंड नहीं है। हल है शुरुआत से ही सही Flow टाइप चुनना।
Kotlin दो hot flow देता है जो कॉल साइट से एक जैसे दिखते हैं — दोनों collectAsStateWithLifecycle() या एक LaunchedEffect से collect होते हैं, दोनों एक ViewModel में रहते हैं, दोनों configuration change से बच निकलते हैं। लेकिन ये एक-दूसरे की जगह नहीं ले सकते, और फर्क बिल्कुल वही है जो ऊपर बताया गया।
StateFlow: हमेशा एक वैल्यू रखता है, हमेशा उसे रीप्ले करता है
StateFlow स्टेट के लिए बना है — ऐसा डेटा जिसकी हर पल एक मौजूदा वैल्यू होती है, चाहे कोई देख रहा हो या नहीं। किसी टाइमर के बचे हुए सेकंड, किसी फॉर्म की validation एरर, किसी लिस्ट का loading/error/success स्टेटस। नए collector तुरंत नवीनतम वैल्यू पाते हैं, और हर collector हमेशा एक जैसी चीज़ देखता है।
private val _uiState = MutableStateFlow(TimerUiState.Idle)
val uiState: StateFlow<TimerUiState> = _uiState.asStateFlow()
fun start(durationMinutes: Int) {
_uiState.value = TimerUiState.Running(
remainingSeconds = durationMinutes * 60,
)
}
“नए collector को हमेशा नवीनतम वैल्यू रीप्ले करना” वाला यह व्यवहार ही StateFlow का पूरा मकसद है — यही वह चीज़ है जो configuration change के बाद Compose स्क्रीन को सही बनाए रखती है। डिवाइस घुमाइए, Composable recompose होता है, एक नया collector अटैच होता है, और वह अगली emission का इंतज़ार करती खाली स्क्रीन की बजाय तुरंत मौजूदा स्टेट रेंडर कर देता है।
SharedFlow: कुछ ऐसा जो हो चुका है, उसे बताता है
वही रीप्ले वाला व्यवहार वन-टाइम इवेंट के लिए बिल्कुल गलत है। मान लीजिए एक Pomodoro सेशन खत्म होता है और आप एक “शाबाश” वाली आवाज़ और एक हल्का-सा सेलिब्रेशन एनिमेशन ट्रिगर करना चाहते हैं — ऐसा कुछ जो एक बार होना चाहिए, जिस पल हुआ उसी पल, हर बार स्क्रीन recompose होने पर नहीं।
अगर इस इवेंट को कम्प्लीशन पर true सेट होने वाले StateFlow<Boolean> की तरह मॉडल किया जाए, तो हर नया collector — रोटेशन के बाद बना हुआ, या उपयोगकर्ता के ऐप छोड़कर वापस आने के बाद बना हुआ भी — तुरंत true देखेगा और सेलिब्रेशन दोबारा चलाएगा। लोग सबसे पहले जिस फिक्स की तरफ जाते हैं वह आमतौर पर एक मैनुअल रीसेट है (consume करने के तुरंत बाद _sessionComplete.value = false), जो तब तक काम करता है जब तक दो collector इसे पढ़ने की रेस न लगाएं, या रीसेट गलत dispatcher पर न चले और फ्लैग समय पर वापस पलटे ही नहीं।
यहाँ सही प्रिमिटिव बिना रीप्ले वाला SharedFlow है, क्योंकि यह कोई “मौजूदा वैल्यू” रखता ही नहीं — यह सिर्फ उन्हीं collector को emit करता है जो emission के पल में सक्रिय रूप से subscribed हों:
private val _events = MutableSharedFlow<TimerEvent>()
val events: SharedFlow<TimerEvent> = _events.asSharedFlow()
private fun onSessionComplete() {
viewModelScope.launch {
_events.emit(TimerEvent.SessionComplete)
}
}
जो collector इवेंट फायर होते वक़्त सुन नहीं रहा था, वह उसे कभी देखता ही नहीं — जो सेलिब्रेशन एनिमेशन के लिए सही है, और टाइमर के स्टेट के लिए गलत होता। यही असमानता पूरा फैसला है: अगर देर से subscribe करने वाले को मौजूदा वैल्यू दिखनी चाहिए, तो वह स्टेट है; अगर उसे वह छूट ही जानी चाहिए थी, तो वह इवेंट है।
वह अंगूठे का नियम जो असल में काम करता है
फैसला लेने के लिए मैं एक ही सवाल पूछता हूँ, और यह हर स्क्रीन पर टिका रहा है जो मैंने इस तरह बनाई है: क्या यह बग होगा अगर एक बिल्कुल नया collector इस वैल्यू को तुरंत पा ले?
- टाइमर स्टेट, फॉर्म का कंटेंट, loading फ्लैग, लिस्ट का कंटेंट → नहीं, एक नए collector को मौजूदा वैल्यू दिखनी चाहिए →
StateFlow. - “यह snackbar दिखाओ,” “इस स्क्रीन पर जाओ,” “यह आवाज़ चलाओ” → हाँ, इसे देर से subscribe करने वाले को रीप्ले करना ही बग है →
SharedFlow(replay = 0).
जानने लायक एक अपवाद है: SharedFlow(replay = 1) मौजूद है और एक बीच का रास्ता जैसा दिखता है, लेकिन असल में यह बस एक StateFlow है जिसमें “हमेशा एक वैल्यू रखने” की गारंटी नहीं है और सिंक्रोनस पढ़ने के लिए .value नहीं है — स्टेट मॉडल करते वक़्त असली StateFlow या इवेंट मॉडल करते वक़्त SharedFlow(replay = 0) की जगह इसकी तरफ जाने की शायद ही कोई वजह होती है।
इसे एक Compose स्क्रीन से जोड़ना
दोनों अलग-अलग तरीके से consume होते हैं, और इसे मिला देना इस बग का दूसरा सबसे आम रूप है:
@Composable
fun TimerScreen(viewModel: TimerViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
TimerEvent.SessionComplete -> playCompletionSound()
}
}
}
TimerContent(uiState)
}
collectAsStateWithLifecycle() StateFlow के लिए है — इसे Compose स्टेट के रूप में दिखाने के लिए एक मौजूदा वैल्यू चाहिए। events को इसके बजाय एक LaunchedEffect के अंदर collect किया जाता है, ठीक इसलिए क्योंकि इसके पास पकड़ने के लिए कोई मौजूदा वैल्यू नहीं है; यह पढ़ने वाली वैल्यू नहीं, बल्कि रिएक्ट करने वाला एक स्ट्रीम है।
निचोड़
यह बग जिस चीज़ को रोकता है वह इतनी बारीक होती है कि आमतौर पर बिना किसी के नोटिस किए शिप हो जाती है — रोटेशन पर डुप्लिकेट टोस्ट, बैकग्राउंड से लौटने के बाद फिर से ट्रिगर होने वाला एनिमेशन — क्योंकि उस आम केस में जहाँ सिर्फ एक collector है जो कभी छोड़ता ही नहीं, सब कुछ ठीक दिखता है। यह तभी सामने आता है जब कोई स्क्रीन अपना collector फिर से शुरू करती है, जो कि हर configuration change पर Compose ठीक यही करता है। स्टेट को StateFlow से मॉडल करें क्योंकि नए collector को catch up करना चाहिए। इवेंट्स को बिना रीप्ले वाले SharedFlow से मॉडल करें क्योंकि उन्हें नहीं करना चाहिए। मैं इस बंटवारे का इस्तेमाल Mintly के टाइमर में करता हूँ, जहाँ चल रही उलटी गिनती स्टेट है जिसे हर recomposition को सही तरीके से दिखाना चाहिए, और सेशन के अंत का सेलिब्रेशन एक इवेंट है जिसे ठीक एक बार फायर होना चाहिए — कभी दो बार नहीं, अभी-अभी दोबारा खुली स्क्रीन पर तो बिल्कुल नहीं।
// संबंधित पठन
जर्नल से और भी
Android का In-App Review API: बिना परेशान किए रेटिंग माँगना
Google के In-App Review API की एक व्यावहारिक गाइड — यह असल में कैसे काम करता है, इसे कब ट्रिगर करना चाहिए, और सामान्य 'हमें रेट करें' पॉपअप आपकी Play Store रेटिंग को चुपचाप कैसे नुकसान पहुँचा रहा है।
Android ऐप शॉर्टकट्स और Quick Settings Tiles: बिना ऐप खोले पानी लॉग करना
Android के डायनामिक ShortcutManager API और TileService की एक व्यावहारिक गाइड — कैसे एक-टैप एक्शन को पूरी तरह ऐप को छोड़कर काम करने दें, साथ ही वे गड़बड़ियाँ जो ज़्यादातर इम्प्लीमेंटेशन को फँसा देती हैं।
2026 में Android पर फोरग्राउंड सर्विस टाइप्स: वह चुनना जो आपके फीचर पर सच में लागू हो
Android के फोरग्राउंड सर्विस टाइप प्रतिबंधों की एक व्यावहारिक गाइड — dataSync, mediaPlayback, specialUse, shortService — और बिना मारे या रिजेक्ट हुए सही वाला कैसे चुनें।