नेटवर्किंग और बैकग्राउंड वर्क के लिए Kotlin Coroutines बनाम RxJava
Kotlin Coroutines बनाम RxJava: रद्दीकरण, एरर हैंडलिंग और परीक्षण पैटर्न्स की तुलना करें जो असली Android ऐप्स में नेटवर्किंग और बैकग्राउंड वर्क के लिए महत्वपूर्ण हैं।

यह चुनाव प्रोडक्शन नेटवर्किंग के लिए क्यों मायने रखता है
एक असली Android ऐप में नेटवर्किंग और बैकग्राउंड वर्क सिर्फ़ एक API कॉल से ज़्यादा होता है। इसमें लॉगिन और टोकन रिफ्रेश, स्क्रीन रोटेशन के बीच लोड, जब यूज़र स्क्रीन छोड़ देता है तब सिंक, फ़ोटो अपलोड्स, और पिरियॉडिक वर्क शामिल हैं जो बैटरी जल्दी न ख़त्म करे।
सबसे झल्लाने वाले बग अक्सर सिंटैक्स की समस्याएँ नहीं होते। वे तब आते हैं जब असिंक वर्क UI से ज़्यादा समय तक चलता है (लीक्स), जब कैंसलेशन UI को रोक दे पर असल अनुरोध नहीं रुकता (बेकार ट्रैफ़िक और अटके स्पिनर), जब retries अनुरोधों को गुणा कर देते हैं (रेट लिमिट्स, बैन), या जब अलग-अलग लेयर एरर्स को अलग तरह से हैंडल करती हैं इसलिए कोई भी नहीं बता पाता कि यूज़र को क्या दिखेगा।
Kotlin Coroutines बनाम RxJava का निर्णय रोज़मर्रा की विश्वसनीयता को प्रभावित करता है:
- आप काम को कैसे मॉडल करते हैं (one-shot कॉल्स बनाम स्ट्रीम)
- कैंसलेशन कैसे फैलता है
- एरर्स कैसे रिप्रेज़ेंट और UI तक पहुंचते हैं
- नेटवर्क, डिस्क और UI के लिए थ्रेड्स को आप कैसे कंट्रोल करते हैं
- टेस्टेबल टाइमिंग, retries, और एज केस कैसे होते हैं
नीचे दिए पैटर्न उन स्थितियों पर ध्यान केंद्रित करते हैं जो लोड या धीने नेटवर्क पर टूटने की प्रवृत्ति रखते हैं: कैंसलेशन, एरर हैंडलिंग, retries और टाइमआउट्स, और टेस्टिंग आदतें जो regressions रोकती हैं। उदाहरण छोटे और प्रैक्टिकल रखे गए हैं।
मूल मानसिक मॉडल: suspend कॉल्स, स्ट्रीम्स, और Flow
Kotlin Coroutines बनाम RxJava का मुख्य अंतर उस काम के आकार में है जिसे आप मॉडल कर रहे हैं।
एक suspend फ़ंक्शन एक single-shot ऑपरेशन को दर्शाता है। यह एक वैल्यू रिटर्न करता है या एक त्रुटि फेंकता है। यह अधिकतर नेटवर्क कॉल्स से मिलता है: प्रोफ़ाइल प्राप्त करना, सेटिंग्स अपडेट करना, फ़ोटो अपलोड। कॉलिंग कोड ऊपर से नीचे पढ़ता है, जो लॉगिंग, कैशिंग और ब्रांचिंग जोड़ने पर भी आसान रहता है।
RxJava सबसे पहले पूछता है कि क्या आप एक वैल्यू से निपट रहे हैं या समय के साथ कई वैल्यूज़। एक Single एक one-shot परिणाम है (सफलता या त्रुटि)। एक Observable (या Flowable) एक स्ट्रीम है जो कई वैल्यूज़ इमिट कर सकती है, फिर कम्प्लीट या फेल हो सकती है। यह उन फीचर्स के लिए उपयुक्त है जो वास्तव में इवेंट-लाइक हों: टेक्स्ट चेंज इवेंट्स, websocket मैसेजेस, या पोलिंग।
Flow coroutine-फ्रेंडली तरीका है स्ट्रीम को रिप्रेजेंट करने का। इसे coroutines का “स्ट्रीम वर्शन” समझें, जिसमें structured cancellation है और यह suspending APIs के साथ डायरेक्ट फिट बैठता है।
एक त्वरित नियम:
- एक अनुरोध और एक उत्तर के लिए
suspendका उपयोग करें। - समय के साथ बदलने वाली वैल्यूज़ के लिए
Flowका उपयोग करें। - जब आपकी ऐप पहले से ही ऑपरेटर्स और जटिल स्ट्रीम कंपोजिशन पर भारी निर्भर हो, तो RxJava का उपयोग करें।
जैसे-जैसे फीचर्स बढ़ते हैं, पढ़ने की क्षमता अक्सर तब पहले टूटती है जब आप एक one-shot कॉल पर स्ट्रीम मॉडल ज़बरदस्ती लागू करते हैं, या आप चल रहे इवेंट्स को एक सिंगल रिटर्न वैल्यू की तरह ट्रीट करने की कोशिश करते हैं। पहले अमूर्तता को वास्तविकता से मिलाएं, फिर उसके चारों ओर कन्वेंशन्स बनाएं।
व्यवहार में कैंसलेशन (संक्षिप्त कोड उदाहरणों के साथ)
कैंसलेशन वह जगह है जहाँ असिंक कोड सुरक्षित महसूस करता है, या रैंडम क्रैश और बेकार कॉल बन जाता है। लक्ष्य सरल है: जब उपयोगकर्ता एक स्क्रीन छोड़ता है, तो उस स्क्रीन के लिए शुरू किया गया कोई भी वर्क रुक जाना चाहिए।
Kotlin Coroutines में कैंसलेशन मॉडल में बिल्ट-इन है। एक Job वर्क का प्रतिनिधित्व करता है, और structured concurrency के साथ आप सामान्यतः Jobs इधर-उधर पास नहीं करते। आप एक स्कोप (जैसे ViewModel स्कोप) के अंदर वर्क शुरू करते हैं। जब वह स्कोप कैंसिल हो जाता है, तो उसके अंदर सब कुछ भी कैंसिल हो जाता है।
class ProfileViewModel(
private val api: Api
) : ViewModel() {
fun loadProfile() = viewModelScope.launch {
// If the ViewModel is cleared, this coroutine is cancelled,
// and so is the in-flight network call (if the client supports it).
val profile = api.getProfile() // suspend
// update UI state here
}
}
दो प्रोडक्शन विवरण महत्वपूर्ण हैं:
- suspend नेटवर्किंग को एक cancellable क्लाइंट के माध्यम से कॉल करें। वरना coroutine रुक सकती है लेकिन HTTP कॉल चलती रहे सकती है।
- उन अनुरोधों के लिए जो हैंग नहीं होने चाहिए,
withTimeout(याwithTimeoutOrNull) का उपयोग करें।
RxJava में स्पष्ट disposal होता है। आप प्रत्येक सब्सक्रिप्शन के लिए एक Disposable रखते हैं, या उन्हें CompositeDisposable में जोड़ते हैं। जब स्क्रीन चला जाता है, आप dispose करते हैं, और चेन को रोक देना चाहिए।
class ProfilePresenter(private val api: ApiRx) {
private val bag = CompositeDisposable()
fun attach() {
bag += api.getProfile()
.subscribe(
{ profile -> /* render */ },
{ error -> /* show error */ }
)
}
fun detach() {
bag.clear() // cancels in-flight work if upstream supports cancellation
}
}
एक व्यावहारिक स्क्रीन-एग्जिट नियम: अगर आप यह नहीं बता सकते कि कैंसलेशन कहाँ होता है (स्कोप कैंसलेशन या dispose()), तो मान लें कि वर्क चलता रहेगा और इसे शिप करने से पहले ठीक करें।
एरर हैंडलिंग जो समझने में आसान रहे
Kotlin Coroutines बनाम RxJava का बड़ा अंतर यह है कि एरर्स कैसे यात्रा करते हैं। Coroutines विफलताओं को सामान्य कोड जैसा बनाते हैं: एक suspend कॉल फेंकती है, और कॉलर तय करता है कि क्या करना है। Rx विफलताओं को स्ट्रीम के माध्यम से धकेलता है, जो शक्तिशाली है, पर अगर आप सावधान नहीं हैं तो समस्याएँ छिप सकती हैं।
अनपेक्षित विफलताओं (टाइमआउट, 500s, पार्सिंग बग) के लिए exceptions का उपयोग करें। जब UI को खास प्रतिक्रिया चाहिए (गलत पासवर्ड, “ईमेल पहले से उपयोग में है”) और आप चाहते हैं कि यह आपके डोमेन मॉडल का हिस्सा बने, तो एरर को डेटा के रूप में मॉडल करें।
एक सरल coroutine पैटर्न stack trace को बनाए रखता है और पठनीय रहता है:
suspend fun loadProfile(): Profile = try {
api.getProfile() // may throw
} catch (e: IOException) {
throw NetworkException("No connection", e)
}
runCatching और Result उपयोगी हैं जब आप वास्तव में success या failure को बिना फेंके लौटाना चाहते हैं:
suspend fun loadProfileResult(): Result<Profile> =
runCatching { api.getProfile() }
यदि आप getOrNull() का उपयोग करते हैं तो सावधान रहें अगर आप विफलता को भी हैंडल नहीं कर रहे—यह असल बग्स को चुपके से “empty state” स्क्रीन में बदल सकता है।
RxJava में एरर पथ को स्पष्ट रखें। केवल सुरक्षित fallback के लिए onErrorReturn का उपयोग करें। जब आपको स्रोत बदलने की ज़रूरत हो (उदा. कैश किए हुए डेटा पर स्विच करने के लिए), तब onErrorResumeNext पसंद करें। retries के लिए, retryWhen के साथ नियम संकरे रखें ताकि आप “गलत पासवर्ड” पर retry न कर दें।
कुछ आदतें जो swallowed errors रोकती हैं:
- जिस जगह पर आपके पास संदर्भ होता है, वहीं एरर को लॉग या रिपोर्ट करें।
- wrapping करते समय मूल exception को
causeके रूप में बनाए रखें। - ऐसे catch-all fallbacks से बचें जो हर एरर को डिफ़ॉल्ट वैल्यू बना दें।
- यूज़र-फेसिंग एरर्स को एक typed मॉडल बनाएं, न कि सिर्फ़ स्ट्रिंग।
थ्रेडिंग बेसिक्स: Dispatchers बनाम Schedulers
कई असिंक बग थ्रेडिंग के कारण होते हैं: मुख्य थ्रेड पर भारी वर्क करना, या बैकग्राउंड थ्रेड से UI को छूना। Kotlin Coroutines बनाम RxJava मुख्यतः बताता है कि आप थ्रेड स्विच कैसे व्यक्त करते हैं।
Coroutines के साथ, आप अक्सर UI वर्क के लिए मुख्य पर शुरू करते हैं, फिर महंगे हिस्सों के लिए बैकग्राउंड डिस्पैचर पर जाते हैं। सामान्य विकल्प हैं:
Dispatchers.MainUI अपडेट्स के लिएDispatchers.IOब्लॉकिंग I/O जैसे नेटवर्किंग और डिस्क के लिएDispatchers.DefaultCPU वर्क जैसे JSON पार्सिंग, सॉर्टिंग, एन्क्रिप्शन के लिए
एक सादा पैटर्न: डेटा फ़ेच करें, मुख्य से बाहर पार्स करें, फिर render करें।
viewModelScope.launch(Dispatchers.Main) {
val json = withContext(Dispatchers.IO) { api.fetchProfileJson() }
val profile = withContext(Dispatchers.Default) { parseProfile(json) }
_uiState.value = UiState.Content(profile)
}
RxJava “कहाँ वर्क होता है” को subscribeOn से और “कहाँ परिणाम देखा जाता है” को observeOn से व्यक्त करता है। एक सामान्य आश्चर्य यह है कि observeOn upstream वर्क को प्रभावित नहीं करता। subscribeOn स्रोत और उसके ऊपर के ऑपरेटर्स के लिए थ्रेड सेट करता है, और हर observeOn वहाँ से आगे के लिए थ्रेड बदल देता है।
api.fetchProfileJson()
.subscribeOn(Schedulers.io())
.map { json -> parseProfile(json) } // still on io unless you change it
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ profile -> render(profile) },
{ error -> showError(error) }
)
एक नियम जो आश्चर्यों से बचाता है: UI वर्क को एक जगह रखें। Coroutines में, UI स्टेट को Dispatchers.Main पर assign या collect करें। RxJava में, render करने से ठीक पहले एक final observeOn(main) लगाएँ और अनावश्यक observeOn कॉल्स न फैलाएँ जब तक वास्तव में ज़रूरत न हो।
यदि कोई स्क्रीन स्टटर करती है, तो पहले पार्सिंग और मैपिंग को मुख्य थ्रेड से बाहर ले जाएँ। यह एक अकेला बदलाव कई रियल-वर्ल्ड इश्यूज़ ठीक कर देता है।
नेटवर्क कॉल्स के लिए रिट्राई, टाइमआउट और पैरेलल वर्क
हैप्पी पाथ शायद ही कभी समस्या हो। समस्याएँ उन कॉल्स से आती हैं जो हैंग हो जाते हैं, retries जो चीज़ों को और खराब कर देते हैं, या “पैरेलल” वर्क जो असल में पैरेलल नहीं होता। ये पैटर्न अक्सर तय करते हैं कि टीम Kotlin Coroutines बनाम RxJava में से किसे पसंद करे।
फास्ट फेल करने वाले टाइमआउट्स
Coroutines के साथ, आप किसी भी suspend कॉल के चारों ओर हार्ड कैप लगा सकते हैं। टाइमआउट को कॉल साइट के नज़दीक रखें ताकि आप सही UI संदेश दिखा सकें।
val user = withTimeout(5_000) {
api.getUser() // suspend
}
RxJava में, आप स्ट्रीम पर एक timeout ऑपरेटर जोड़ते हैं। यह उपयोगी होता है जब timeout व्यवहार शेयर किए गए पाइपलाइन का हिस्सा होना चाहिए।
नुकसान पहुँचाए बिना रिट्राई
केवल तब ही retry करें जब retry करना सुरक्षित हो। एक सरल नियम: idempotent अनुरोध (जैसे GET) को सापेक्ष रूप से अधिक स्वतंत्रता के साथ retry करें बनाम ऐसे अनुरोध जिन्हें साइड-इफ़ेक्ट होते हैं (जैसे “ऑर्डर बनाएं”)। फिर भी, संख्या को सीमित रखें और delay या jitter जोड़ें।
अच्छे डिफ़ॉल्ट गार्डरेिल्स:
- नेटवर्क टाइमआउट और अस्थायी सर्वर एरर्स पर retry करें।
- वेलिडेशन एरर्स (400s) या ऑथ फेल्युअर्स पर retry न करें।
- retries की कैप सेट करें (अक्सर 2–3) और अंतिम विफलता को लॉग करें।
- बैकऑफ डिले रखें ताकि आप सर्वर को हैमर न करें।
RxJava में retryWhen आपको "इन एरर्स पर और इस डिले के साथ retry" व्यक्त करने देता है। Coroutines में, Flow के पास retry और retryWhen होते हैं, जबकि सादा suspend फ़ंक्शन्स अक्सर छोटे लूप और delays का उपयोग करते हैं।
उलझे हुए कोड के बिना पैरेलल कॉल्स
Coroutines पैरेलल वर्क को डायरेक्ट बनाते हैं: दो अनुरोध शुरू करें, दोनों का इंतज़ार करें।
coroutineScope {
val profile = async { api.getProfile() }
val feed = async { api.getFeed() }
profile.await() to feed.await()
}
RxJava वहाँ पर चमकता है जहाँ कई स्रोतों को मिलाना चेन का मुख्य उद्देश्य हो। zip आम तौर पर "दोनों के लिए प्रतीक्षा करें" का साधन है, और merge उपयोगी है जब आप परिणाम आते ही उन्हें चाहते हैं।
बड़े या तेज़ स्ट्रीम्स के लिए, बैकप्रेशर अभी भी मायने रखता है। RxJava का Flowable परिपक्व बैकप्रेशर टूल्स देता है। Coroutines Flow कई मामलों को अच्छी तरह हैंडल करता है, पर अगर इवेंट्स आपकी UI या DB लिखाई से तेज़ी से आ सकते हैं तो आपको buffering या dropping नीतियों की ज़रूरत पड़ सकती है।
इंटरऑप और माइग्रेशन पैटर्न्स (मिक्स्ड कोडबेस)
ज़्यादातर टीमें एकदम से स्वीच नहीं करतीं। एक व्यावहारिक Kotlin Coroutines बनाम RxJava माइग्रेशन ऐप को स्थिर रखते हुए मॉड्यूल-बाय-मॉड्यूल मूव करने की सलाह देती है।
Rx API को suspend फ़ंक्शन में रैप करें
यदि आपके पास मौजूदा Single<T> या Completable है, तो इसे कैंसलेशन सपोर्ट के साथ रैप करें ताकि कैंसिल हुई coroutine Rx सब्सक्रिप्शन dispose कर दे।
suspend fun <T : Any> Single<T>.awaitCancellable(): T =
suspendCancellableCoroutine { cont ->
val d = subscribe(
{ value -> cont.resume(value) {} },
{ error -> cont.resumeWithException(error) }
)
cont.invokeOnCancellation { d.dispose() }
}
यह एक आम फेलियर मोड से बचाता है: यूज़र स्क्रीन छोड़ देता है, coroutine कैंसिल हो जाती है, पर नेटवर्क कॉल चलती रहती है और बाद में साझा स्टेट अपडेट कर देती है।
Coroutine को Rx कॉलर्स को एक्सपोज़ करें
माइग्रेशन के दौरान, कुछ लेयर्स अभी भी Rx टाइप्स की उम्मीद करेंगी। suspend वर्क को Single.fromCallable में रैप करें और केवल बैकग्राउंड थ्रेड पर ब्लॉक करें।
fun loadProfileRx(api: Api): Single<Profile> =
Single.fromCallable {
runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
}
इस बॉउंडरी को छोटा और डॉक्यूमेंटेड रखें। नए कोड के लिए, suspend API को सीधे coroutine स्कोप से कॉल करना पसंद करें।
Flow कहाँ फिट बैठता है, और कहाँ नहीं
Flow कई Observable उपयोग मामलों को बदल सकता है: UI स्टेट, डेटाबेस अपडेट्स, और paging-जैसी स्ट्रीम्स। यह कम डायरेक्ट हो सकता है अगर आप भारी रूप से hot streams, subjects, एडवांस्ड बैकप्रेशर ट्यूनिंग, या बहुत से कस्टम ऑपरेटर्स पर निर्भर करते हैं जिन्हें आपकी टीम पहले से जानती है।
एक माइग्रेशन रणनीति जो भ्रम कम रखे:
- लीफ मॉड्यूल्स पहले (नेटवर्क, स्टोरेज) को suspend APIs में कन्वर्ट करें।
- मॉड्यूल बाउंड्रीज़ पर छोटे adapters जोड़ें (Rx से suspend, suspend से Rx)।
- जहां आप consumers को भी कंट्रोल करते हों, वहां Rx स्ट्रीम्स को Flow से बदलें।
- हर फीचर एरिया के लिए एक असिंक स्टाइल रखें।
- जैसे ही आख़िरी कॉलर माइग्रेट हो जाए, adapters हटाएँ।
ऐसे टेस्टिंग पैटर्न जो आप वास्तव में उपयोग करेंगे
टाइमिंग और कैंसलेशन इश्यूज़ वे जगहें हैं जहाँ असिंक बग छिपते हैं। अच्छे असिंक टेस्ट समय को निर्णायक बनाते हैं और आउटपुट को असर्ट करना आसान करते हैं। यह भी वह क्षेत्र है जहाँ Kotlin Coroutines बनाम RxJava अलग महसूस होते हैं, हालांकि दोनों अच्छे से टेस्ट किए जा सकते हैं।
Coroutines: runTest, TestDispatcher, और समय को नियंत्रित करना
Coroutine कोड के लिए, runTest को एक टेस्ट डिस्पैचर के साथ पसंद करें ताकि आपका टेस्ट असली थ्रेड्स या असली डिले पर निर्भर न करे। वर्चुअल समय आपको टाइमआउट्स, retries, और debounce विंडोज़ ट्रिगर करने देता है बिना सोए हुए।
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `emits Loading then Success`() = runTest {
val dispatcher = StandardTestDispatcher(testScheduler)
val repo = Repo(api = fakeApi, io = dispatcher)
val states = mutableListOf<UiState>()
val job = launch(dispatcher) { repo.loadProfile().toList(states) }
testScheduler.runCurrent() // run queued work
assert(states.first() is UiState.Loading)
testScheduler.advanceTimeBy(1_000) // trigger delay/retry windows
testScheduler.runCurrent()
assert(states.last() is UiState.Success)
job.cancel()
}
कैंसलेशन टेस्ट करने के लिए, collecting Job (या parent स्कोप) को कैंसिल करें और अपने fake API के रुकने या और स्टेट्स न इमिट होने का assert करें।
RxJava: TestScheduler, TestObserver, निर्णायक समय
Rx टेस्ट आम तौर पर TestScheduler (समय के लिए) और TestObserver (असर्शन के लिए) का संयोजन करते हैं।
@Test
fun `disposes on cancel and stops emissions`() {
val scheduler = TestScheduler()
val observer = TestObserver<UiState>()
val d = repo.loadProfileRx(scheduler)
.subscribeWith(observer)
scheduler.triggerActions()
observer.assertValueAt(0) { it is UiState.Loading }
d.dispose()
scheduler.advanceTimeBy(1, TimeUnit.SECONDS)
observer.assertValueCount(1) // no more events after dispose
}
किसी भी स्टाइल में एरर पाथ्स टेस्ट करते समय, एक्सेप्शन के प्रकार पर नहीं बल्कि मैपिंग पर ध्यान दें। अपेक्षित UI स्टेट को assert करें जैसे 401, टाइमआउट, या malformed response के बाद।
कुछ छोटे चेक्स अधिकांश regressions को कवर करते हैं:
- Loading और अंतिम स्टेट्स (Success, Empty, Error)
- Cancellation क्लीनअप (job cancelled, disposable disposed)
- Error मैपिंग (सर्वर कोड से यूज़र मैसेज)
- Retries के बाद duplicate emissions न हों
- टाइम-बेस्ड लॉजिक वर्चुअल टाइम का उपयोग करे, असली डिले नहीं
प्रोडक्शन बग्स पैदा करने वाली सामान्य गलतियाँ
अधिकांश प्रोडक्शन इश्यूज़ Kotlin Coroutines बनाम RxJava चुनने से नहीं आते। वे कुछ आदतों से आते हैं जो काम को आपकी समझ से ज़्यादा देर तक चलने देते हैं, दो बार चलाते हैं, या गलत समय पर UI को छू लेते हैं।
एक सामान्य लीक यह है कि गलत स्कोप में वर्क लॉन्च करना। यदि आप किसी ऐसे स्कोप से नेटवर्क कॉल शुरू करते हैं जो स्क्रीन से लंबा रहता है (या आप अपना खुद का स्कोप बनाते हैं और कभी इसे कैंसिल नहीं करते), तो अनुरोध यूज़र के निकलने के बाद भी पूरा हो सकता है और फिर भी स्टेट अपडेट कर सकता है। Coroutines में यह अक्सर लंबे-जीवित स्कोप का डिफ़ॉल्ट उपयोग दिखता है। RxJava में यह आमतौर पर मिस्ड dispose होता है।
एक और क्लासिक गलती है "fire and forget"। ग्लोबल स्कोप्स और भूले हुए Disposables तब तक ठीक लगते हैं जब तक कि वर्क इकट्ठा न हो जाए। एक चैट स्क्रीन जो हर resume पर रिफ्रेश करती है, कुछ नेविगेशन्स के बाद कई रिफ्रेश जॉब्स चला सकती है, जो मेमोरी होल्ड करते हैं और नेटवर्क पर प्रतिस्पर्धा करते हैं।
Retries भी गलत करना आसान है। अनलिमिटेड retries, या बिना डिले के retries, आपके बैकएंड को स्पैम कर सकते हैं और बैटरी ड्रेन कर सकते हैं। यह विशेष रूप से खतरناک है जब फ़ेल्योर स्थायी हो, जैसे logout के बाद 401। retries को कंडीशनल रखें, बैकऑफ जोड़ें, और उस समय बंद करें जब एरर recoverable न हो।
थ्रेडिंग गलतियाँ ऐसे क्रैशेस पैदा करती हैं जिन्हें reproduce करना कठिन होता है। आप मुख्य थ्रेड पर JSON पार्स कर रहे होंगे या बैकग्राउंड से UI अपडेट कर रहे होंगे यह इस बात पर निर्भर करेगा कि आपने डिस्पैचर या scheduler कहाँ रखा है।
तेज़ जाँचें जो इन मुद्दों का अधिकांश भाग पकड़ लेते हैं:
- वर्क को lifecycle owner से बाँधें और उस owner के समाप्त होने पर इसे कैंसिल करें।
- क्लीनअप को स्पष्ट बनाएं: Jobs कैंसिल करें या Disposables एक ही जगह क्लियर करें।
- Retries पर कड़े सीमाएँ रखें (काउंट, डिले, और कौन से एरर्स योग्य हैं)।
- UI अपडेट्स के लिए एक नियम लागू करें (सिर्फ़ main थ्रेड) कोड रिव्यूज़ में।
- बैकग्राउंड सिंक को एक प्रणाली की तरह मानें जिसमें constraints हों, न कि एक यादृच्छिक फ़ंक्शन कॉल।
यदि आप जेनरेट किए गए Kotlin कोड (उदा. AppMaster) से Android ऐप्स शिप करते हैं, तो वही पिटफॉल्स लागू होते हैं। आपको स्कॉप्स, कैंसलेशन, retry लिमिट्स और थ्रेड नियमों के लिए स्पष्ट कन्वेंशन्स की ज़रूरत होगी।
Coroutines, RxJava, या दोनों चुनने के लिए त्वरित चेकलिस्ट
काम के आकार से शुरू करें। अधिकतर नेटवर्किंग कॉल्स single-shot होते हैं, पर ऐप्स में ओngoing सिग्नल भी होते हैं जैसे कनेक्टिविटी, ऑथ स्टेट, या लाइव अपडेट्स। प्रारंभ में गलत अमूर्तता का चयन अक्सर बाद में messy कैंसलेशन और कठिन एरर पाथ्स के रूप में सामने आता है।
टीम को समझाने और निर्णय लेने का एक सरल तरीका:
- एक-बार का अनुरोध (login, fetch profile):
suspendफ़ंक्शन पसंद करें। - चलती स्ट्रीम (इवेंट्स, DB अपडेट्स):
Flowया RxObservableपसंद करें। - UI लाइफसाइकल कैंसलेशन:
viewModelScopeयाlifecycleScopeमें coroutines अक्सर मैन्युअल disposables से सरल होते हैं। - एडवांस्ड स्ट्रीम ऑपरेटर्स और बैकप्रेशर पर भारी भरोसा: RxJava अभी भी बेहतर फिट हो सकता है, विशेषकर पुराने कोडबेस में।
- जटिल retries और एरर मैपिंग: उस अप्रोच को चुनें जिसे आपकी टीम पठनीय रख सकती है।
एक व्यावहारिक नियम: अगर एक स्क्रीन एक अनुरोध करती है और एक परिणाम रेंडर करती है, तो coroutines कोड को सामान्य फ़ंक्शन कॉल के करीब रखता है। अगर आप कई इवेंट्स की पाइपलाइन बना रहे हैं (typing, debounce, previous requests को cancel करना, filters को combine करना), तो RxJava या Flow अधिक प्राकृतिक महसूस हो सकते हैं।
सुसंगतता परफेक्शन से बेहतर है। दो अच्छे पैटर्न पूरे एप में उपयोग किए जाने पर पाँच “बेस्ट” पैटर्न्स से बेहतर सपोर्टेबल होते हैं।
उदाहरण परिदृश्य: लॉगिन, प्रोफ़ाइल फ़ेच और बैकग्राउंड सिंक
एक सामान्य प्रोडक्शन फ़्लो है: यूज़र लॉगिन टैप करता है, आप auth endpoint कॉल करते हैं, फिर होम स्क्रीन के लिए प्रोफ़ाइल फ़ेच करते हैं, और अंत में बैकग्राउंड सिंक शुरू करते हैं। यह वह जगह है जहाँ Kotlin Coroutines बनाम RxJava दिन-प्रतिदिन में मेंटेनेंस में अलग महसूस करा सकता है।
Coroutines वर्शन (सीक्वेंशियल + कैंसलेबल)
Coroutines के साथ, "यह करो, फिर वह करो" आकार नेचुरल है। अगर यूज़र स्क्रीन बंद कर देता है, तो स्कोप कैंसिल होने से इन-फ्लाइट वर्क रुक जाता है।
suspend fun loginAndLoadProfile(): Result<Profile> = runCatching {
val token = api.login(email, password) // suspend
val profile = api.profile("Bearer $token")
syncManager.startSyncInBackground(token) // fire-and-forget
profile
}.recoverCatching { e ->
throw when (e) {
is HttpException -> when (e.code()) {
401 -> AuthExpiredException()
in 500..599 -> ServerDownException()
else -> e
}
is IOException -> NoNetworkException()
else -> e
}
}
// UI layer
val job = viewModelScope.launch { loginAndLoadProfile() }
override fun onCleared() { job.cancel() }
RxJava वर्शन (चेन + डिस्पोज़ल)
RxJava में, वही फ़्लो एक चेन है। कैंसलेशन का अर्थ डिस्पोज़ करना है, आमतौर पर CompositeDisposable के साथ।
val d = api.login(email, password)
.flatMap { token -> api.profile("Bearer $token").map { it to token } }
.doOnSuccess { (_, token) -> syncManager.startSyncInBackground(token) }
.onErrorResumeNext { e: Throwable ->
Single.error(
when (e) {
is HttpException -> if (e.code() == 401) AuthExpiredException() else e
is IOException -> NoNetworkException()
else -> e
}
)
}
.subscribe({ (profile, _) -> show(profile) }, { showError(it) })
compositeDisposable.add(d)
override fun onCleared() { compositeDisposable.clear() }
यहाँ एक मिनिमल टेस्ट सूट को तीन आउटकम कवर करने चाहिए: सफलता, मैप्ड फेल्यूर (401, 500s, नो नेटवर्क), और कैंसलेशन/डिस्पोज़ल।
अगले कदम: कन्वेंशन्स चुनें और उन्हें सुसंगत रखें
टीमें आमतौर पर इसलिए समस्या में पड़ती हैं क्योंकि पैटर्न फीचर्स के बीच बदलते हैं, न कि इसलिए कि Kotlin Coroutines बनाम RxJava स्वाभाविक रूप से "गलत" है। एक छोटा निर्णय नोट (यहाँ तक कि एक पेज) रिव्यूज़ में समय बचाता है और व्यवहार को अनुमान्य बनाता है।
सभी से शुरू करें: one-shot वर्क (एक सिंगल नेटवर्क कॉल जो एक बार रिटर्न करता है) बनाम स्ट्रीम्स (समय के साथ अपडेट्स, जैसे websocket इवेंट्स, लोकेशन, या DB परिवर्तन)। प्रत्येक के लिए एक डिफ़ॉल्ट तय करें, और कब अपवाद की अनुमति है यह परिभाषित करें।
फिर कुछ साझा हैल्पर्स जोड़ें ताकि हर फीचर नेटवर्क खराब होने पर एक जैसा व्यवहार करे:
- HTTP कोड्स, टाइमआउट्स, ऑफ़लाइन जैसी चीज़ों को ऐप-लेवल फ़ेल्यर्स में मैप करने की एक जगह
- नेटवर्क कॉल्स के लिए डिफ़ॉल्ट टाइमआउट मान, और लंबी ऑपरेशन्स के लिए ओवरराइड का स्पष्ट तरीका
- एक retry पॉलिसी जो बताती हो कि क्या सुरक्षित है (उदा. GET बनाम POST)
- एक कैंसलेशन नियम: क्या यूज़र स्क्रीन छोड़ने पर रुकता है और क्या जारी रहने की अनुमति है
- सपोर्ट के लिए लॉगिंग नियम जो संवेदनशील डेटा लीक न करें
टेस्टिंग कन्वेंशन्स उतने ही महत्वपूर्ण हैं। मान लें कि टेस्ट्स असली समय या थ्रेड्स पर निर्भर न हों। Coroutines के लिए, आमतौर पर टेस्ट डिस्पैचर और स्ट्रक्चर्ड स्कोप; RxJava के लिए, टेस्ट schedulers और स्पष्ट disposal। किसी भी तरह, तेज़, निर्णायक टेस्ट जिनमें कोई sleep न हो, उन्हें लक्ष्य बनाएं।
अगर आप समग्र गति बढ़ाना चाहते हैं, तो AppMaster (appmaster.io) एक विकल्प है जो बैकएंड APIs और Kotlin-आधारित मोबाइल ऐप्स को जनरेट करने में मदद करता है बिना सब कुछ फ्रॉम-स्क्रैच लिखे। जेनरेट किए गए कोड के साथ भी वही प्रोडक्शन कन्वेंशन्स—कैंसलेशन, एरर, retries और टेस्टिंग—वे चीज़ें हैं जो नेटवर्किंग व्यवहार को अनुमान्य बनाए रखती हैं।
सामान्य प्रश्न
डिफ़ॉल्ट रूप से suspend का उपयोग उन एक अनुरोधों के लिए करें जो एक बार रिटर्न करते हैं, जैसे लॉगिन या प्रोफ़ाइल फ़ेच करना। जब मानों का समय के साथ परिवर्तन हो (जैसे websocket संदेश, कनेक्टिविटी, या डेटाबेस अपडेट्स), तब Flow (या Rx स्ट्रीम) उपयुक्त है।
हाँ, पर केवल तब जब आपका HTTP क्लाइंट cancel-सपोर्ट करता हो। कोरोटीन स्कोप रद्द होने पर coroutine रुकेगी, लेकिन यदि नीचे का HTTP कॉल कैंसलेशन-सक्षम नहीं है तो अनुरोध बैकग्राउंड में जारी रह सकता है।
वर्क को किसी lifecycle स्कोप (जैसे viewModelScope) से बांधें ताकि स्क्रीन लॉजिक समाप्त होते ही वह कैंसिल हो जाए। लंबे-जीवित या ग्लोबल स्कोप में लॉन्च करने से बचें जब तक वर्क वास्तव में ऐप-व्यापी न हो।
कोरोटीन में विफ़लताएँ आम तौर पर try/catch से हैंडल होती हैं और उन्हें UI स्टेट में मैप करने के नजदीक रखा जाना चाहिए। RxJava में त्रुटियाँ स्ट्रीम के माध्यम से जाती हैं, इसलिए एरर पाथ को स्पष्ट रखें और उन ऑपरेटर्स से बचें जो चुपके से विफलताओं को डिफ़ॉल्ट वैल्यू में बदल देते हैं।
अनपेक्षित विफलताओं (टाइमआउट, 500s, पार्सिंग इश्यू) के लिए एक्सेप्शंस का उपयोग करें। जब UI को खास प्रतिक्रिया चाहिए (जैसे “गलत पासवर्ड” या “ईमेल पहले से उपयोग में है”), तो टाइप्ड एरर डेटा मॉडल का उपयोग करें ताकि आप स्ट्रिंग मैचिंग पर निर्भर न रहें।
जहाँ आप सही UI संदेश दिखा सकें, वहीं पर टाइमआउट लागू करें—कॉल साइट के नज़दीक। कोरोटीन में withTimeout सीधे suspend कॉल पर काम करता है; RxJava में timeout ऑपरेटर को चेन का हिस्सा बना देता है।
सिर्फ़ उन मामलों में ही retry करें जहाँ यह सुरक्षित हो—आमतौर पर GET जैसे idempotent अनुरोधों पर। retries की संख्या सीमित रखें (2–3), वेलिडेशन एरर्स या ऑथ फेल्युअर्स पर retry न करें, और सर्वर को स्पैम न करने हेतु डिले/जिटर जोड़ें।
सबसे बड़ा पिटफॉल यह है कि आप मुख्य थ्रेड पर भारी काम कर दें या बैकग्राउंड से UI को टच करें। कोरोटीन में Dispatchers.Main UI के लिए और Dispatchers.IO/Default पर्सिंग/CPU वर्क के लिए उपयोग करें। RxJava में subscribeOn स्रोत के लिए थ्रेड सेट करता है और observeOn जहाँ परिणाम देखे जाएं वहां थ्रेड बदलता है—UI के ठीक पहले एक final observeOn(main) रखें।
हाँ—लेकिन इसे छोटा और कैंसलेशन-सक्षम रखें। Rx को suspend में रैप करते समय यह सुनिश्चित करें कि coroutine कैंसिल होने पर Rx सब्सक्रिप्शन dispose हो जाए, और Rx कॉलर्स के लिए suspend वर्क को एक्सपोज़ करते समय केवल सीमित ब्रिज रखें।
वर्चुअल समय का उपयोग करें ताकि टेस्ट स्लीप पर निर्भर न हों। कोरोटीन के लिए runTest और एक टेस्ट डिस्पैचर का उपयोग करें ताकि आप डिले, retries और कैंसलेशन नियंत्रित कर सकें; RxJava के लिए TestScheduler और TestObserver का उपयोग करें और dispose() के बाद कोई emissions ना हों, इसकी पुष्टि करें।


