Kotlin Coroutines বনাম RxJava: নেটওয়ার্কিং ও ব্যাকগ্রাউন্ড কাজের জন্য তুলনা
Kotlin Coroutines বনাম RxJava: প্রোডাকশনে নেটওয়ার্কিং ও ব্যাকগ্রাউন্ড কাজে ক্যান্সেলেশন, ত্রুটি হ্যান্ডলিং এবং টেস্টিং প্যাটার্ন তুলনা করুন।

কেন প্রোডাকশনে এই সিদ্ধান্ত গুরুত্বপূর্ণ
রিয়েল Android অ্যাপে নেটওয়ার্কিং এবং ব্যাকগ্রাউন্ড কাজ মানে স্রেফ একটি API কল নয়। এতে লগইন ও টোকেন রিফ্রেশ, লোডের মধ্যে রোটেট হওয়া স্ক্রীন, ইউজার স্ক্রীন ছেড়ে গেলে সিঙ্ক থাকা, ফটো আপলোড, এবং সময় ভিত্তিক কাজ যা ব্যাটারি খাওয়ানো উচিত নয়—এসব অন্তর্ভুক্ত।
সবচেয়ে ক্ষতিকর বাগগুলো সাধারণত সিনট্যাক্সের সমস্যায় থাকে না। এগুলো তখন দেখা দেয় যখন অ্যাসিঙ্ক কাজ UI থেকে বেশি সময় ধরে বেঁচে থাকে (লীকে), যখন ক্যান্সেল করলে UI বন্ধ হয় কিন্তু প্রকৃত অনুরোধ চলে থাকে (অপ্রয়োজনীয় ট্রাফিক ও আটকে থাকা স্পিনার), যখন রিট্রাইগুলো অনুরোধ বাড়ায় (রেট লিমিট, ব্যান), বা যখন বিভিন্ন স্তর আলাদা ভাবে ত্রুটি হ্যান্ডল করে ফলে কেউই জানে না ব্যবহারকারী কি দেখবে।
Kotlin Coroutines vs RxJava সিদ্ধান্ত দৈনন্দিন নির্ভরশীলতাকে প্রভাবিত করে:
- কাজ কিভাবে মডেল করবেন (একক কল বনাম স্ট্রিম)
- ক্যান্সেলেশন কিভাবে প্রসারিত হয়
- ত্রুটিগুলো কিভাবে উপস্থাপিত হয় এবং UI-তে পৌঁছে
- নেটওয়ার্ক, ডিস্ক, এবং UI-এর জন্য থ্রেডিং কিভাবে নিয়ন্ত্রণ করবেন
- টাইমিং, রিট্রাই, এবং এজ কেসের টেস্টেবিলিটি কেমন
নীচের প্যাটার্নগুলো মূলত যেসব জিনিস লোডে বা ধীর নেটওয়ার্কে ভেঙে যায় সেইগুলোকে লক্ষ্য করে: ক্যান্সেলেশন, ত্রুটি হ্যান্ডলিং, রিট্রাই ও টাইমআউট, এবং রিগ্রেশন রোধ করার টেস্টিং অভ্যাস। উদাহরণগুলো সংক্ষিপ্ত ও ব্যবহারিক রাখা হয়েছে।
মূল মানসিক মডেল: suspend কল, স্ট্রিম, এবং Flow
Kotlin Coroutines vs RxJava-এর প্রধান পার্থক্য হল আপনি যেই কাজের আকৃতি মডেল করছেন সেটি।
একটি suspend ফাংশন একক-শট অপারেশন উপস্থাপন করে। এটি একটি মান ফেরত দেয় বা একটি ব্যর্থতা থ্রো করে। বেশিরভাগ নেটওয়ার্কিং কলেই এটি মিলে: প্রোফাইল ফেচ, সেটিংস আপডেট, ফটো আপলোড। কলিং কোড উপরে-থেকে-নীচে পড়ে, যা লগিং, ক্যাশিং, এবং ব্রাঞ্চিং যোগ করলেও সহজে স্ক্যানযোগ্য থাকে।
RxJava প্রথমে জিজ্ঞেস করে আপনি একটাই মান পাচ্ছেন নাকি সময়ের উপর অনেক মান। একটি Single একক-শট রেজাল্ট (সাকসেস বা এরর)। একটি Observable (বা Flowable) হল একটি স্ট্রিম যা অনেক মান ইমিট করতে পারে, তারপর সম্পন্ন হয়, বা ব্যর্থ হয়। এটি টেক্সট চেঞ্জ ইভেন্ট, ওয়েবসকেট মেসেজ, বা পোলিংয়ের মতো ঘটনা-ভিত্তিক ফিচারের সঙ্গে মানানসই।
Flow হল coroutine-ফ্রেন্ডলি স্ট্রিম উপস্থাপনা। এটিকে coroutines-এর “স্ট্রিম ভার্সন” ভাবুন, যার স্ট্রাকচার্ড ক্যান্সেলেশন রয়েছে এবং suspend API-গুলোর সঙ্গে সরাসরি মানায়।
একটি দ্রুত নিয়ম:
- এক অনুরোধ ও এক রেসপন্সের জন্য
suspendব্যবহার করুন। - সময়ে পরিবর্তিত মানগুলোর জন্য
Flowব্যবহার করুন। - যদি আপনার অ্যাপ ইতিমধ্যেই অপারেটর ও জটিল স্ট্রিম কম্পোজিশনে ব্যাপকভাবে নির্ভর করে, তখন RxJava ব্যবহার করুন।
ফিচার বাড়ার সঙ্গে পড়ার সুবিধা সাধারণত প্রথমে ভেঙে যায় যখন আপনি একক-শট কলকে স্ট্রিম মডেলে জোর করেন, বা চলমান ইভেন্টগুলোকে একক রিটার্ন ভ্যালু হিসেবে আচরণ করার চেষ্টা করেন। প্রথমে বাস্তবতার সঙ্গে বিমূর্ততা মিলান, তারপর তা নিয়ে কনভেনশন বানান।
ক্যান্সেলেশন অনুশীলনে (সংক্ষিপ্ত কোড উদাহরণসহ)
ক্যান্সেলেশন হল যে জায়গায় অ্যাসিঙ্ক কোড নিরাপদ মনে হয়, অথবা এলোমেলো ক্র্যাশ ও অপ্রয়োজনীয় কলের দিকে নিয়ে যায়। লক্ষ্য সহজ: ব্যবহারকারী যখন একটি স্ক্রীন ছেড়ে যায়, সেই স্ক্রীনের জন্য শুরু হওয়া যেকোন কাজ বন্ধ হয়ে যাওয়া উচিত।
Kotlin Coroutines-এ ক্যান্সেলেশন মডেলে তৈরি। একটি Job কাজ উপস্থাপন করে, এবং স্ট্রাকচার্ড কনকারেন্সির সাথে আপনি সাধারণত জব বাইরে পাঠান না। আপনি একটি স্কোপে (যেমন ViewModel scope) কাজ শুরু করেন। সেই স্কোপ ক্যান্সেল হলে, এর ভিতরের সব কিছু ক্যান্সেল হয়ে যায়।
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 নেটওয়ার্কিং ক্যান্সেলেবল ক্লায়েন্টের মাধ্যমে কল করুন। নইলে, coroutine থেমে যাবে কিন্তু HTTP কল কাজ চালিয়ে যেতে পারে।
withTimeout(বাwithTimeoutOrNull) ব্যবহার করুন সেই রিকোয়েস্টগুলোর জন্য যেগুলো ঝুলে থাকার পক্ষে নয়।
RxJava-তে স্পষ্ট disposal ব্যবহার হয়। আপনি প্রতিটি subscription-এর জন্য একটি 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 vs RxJava-তে বড় পার্থক্য হল কিভাবে ত্রুটিগুলো ভ্রমণ করে। Coroutines ব্যর্থতাকে স্বাভাবিক কোডের মতো করে তোলে: একটি suspend কল থ্রো করে, এবং কলার সিদ্ধান্ত নেয় কী করা হবে। Rx ত্রুটিকে স্ট্রিমের মাধ্যমে পাঠায়, যা শক্তিশালী, কিন্তু সাবধানে না করলেই সমস্যা লুকিয়ে যাবে।
অপ্রত্যাশিত ব্যর্থতার জন্য exception ব্যবহার করুন (টাইমআউট, 500s, পার্সিং বাগ)। যখন UI-কে নির্দিষ্ট রেসপন্স দরকার (ভুল পাসওয়ার্ড, “ইমেল আগে ব্যবহৃত”) এবং আপনি চান সেটি আপনার ডোমেন মডেলের অংশ হোক, তখন ত্রুটিকে ডেটা হিসাবে মডেল করুন।
একটি সহজ coroutine প্যাটার্ন স্ট্যাক ট্রেস রাখে এবং পড়তে সহজ থাকে:
suspend fun loadProfile(): Profile = try {
api.getProfile() // may throw
} catch (e: IOException) {
throw NetworkException("No connection", e)
}
runCatching এবং Result তখনই উপযোগী যখন আপনি সত্যিই সাকসেস বা ফেলিয়ারের মধ্যে ফিরতে চান বশর্তে না থ্রো করে:
suspend fun loadProfileResult(): Result<Profile> =
runCatching { api.getProfile() }
আপনি যদি ফেলিয়ারও হ্যান্ডল না করে getOrNull() ব্যবহার করেন তবে সাবধান থাকুন। এটি বাস্তব বাগগুলোকে নীরবে “খালি স্টেট” স্ক্রীনে পরিণত করতে পারে।
RxJava-তে error path স্পষ্ট রাখুন। কেবলই নিরাপদ fallback-এর জন্য onErrorReturn ব্যবহার করুন। যখন আপনি সোর্স পরিবর্তন করতে চান (উদাহরণস্বরূপ cached data), তখন onErrorResumeNext পছন্দ করুন। রিট্রাইয়ের জন্য retryWhen-কে সীমিত নিয়মে রাখুন যাতে আপনি অনিচ্ছাকৃতভাবে “ভুল পাসওয়ার্ড”-এ রিট্রাই না করেন।
কিছু অভ্যাস যা swallowed errors প্রতিরোধ করে:
- যেখানে প্রাসঙ্গিক থাকে সেখানেই একবার লোগ বা রিপোর্ট করুন।
- র্যাপ করার সময় মূল exception-কে
causeহিসেবে রাখুন। - সব ত্রুটিকেই ডিফল্ট ভ্যালুতে পরিণত করে দেওয়া এমন catch-all fallback এড়িয়ে চলুন।
- ব্যবহারকারী-সম্মুখীন ত্রুটিকে টাইপ করা মডেল রাখুন, স্ট্রিং নয়।
থ্রেডিং বেসিক্স: Dispatchers বনাম Schedulers
অনেক অ্যাসিঙ্ক বাগ থ্রেডিংয়ের ওপর নির্ভর করে: মেইন থ্রেডে ভারী কাজ করা, অথবা ব্যাকগ্রাউন্ড থ্রেড থেকে UI টাচ করা। Kotlin Coroutines vs RxJava প্রধানত থ্রেড সুইচ কিভাবে প্রকাশ করবেন তা ভিন্ন করে।
Coroutines-এ, আপনি সাধারণত UI কাজের জন্য মেইন থ্রেডে শুরু করেন, তারপর ব্যাকগ্রাউন্ড dispatcher-এ চলে যান খরচসাপেক্ষ অংশের জন্য। সাধারণ পছন্দগুলো:
Dispatchers.MainUI আপডেটের জন্যDispatchers.IOব্লকিং I/O যেমন নেটওয়ার্কিং ও ডিস্ক জন্যDispatchers.DefaultCPU কাজের জন্য যেমন JSON পার্সিং, সোর্টিং, এনক্রিপশন
একটি সরল প্যাটার্ন: ডাটা ফেচ করুন, মেইন থ্রেড থেকে parsing নিয়ে off করুন, তারপর রেন্ডার।
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 সেই পয়েন্ট থেকে onward থ্রেড পরিবর্তন করে।
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 state Dispatchers.Main-এ assign বা collect করুন। RxJava-তে, রেন্ডারিংয়ের ঠিক আগে একটি final observeOn(main) রাখুন, এবং অতিরিক্ত observeOn এড়িয়ে চলুন যদি না সত্যিই দরকার হয়।
যদি একটি স্ক্রীন স্টাটার করে, প্রথমে parsing ও mapping মেইন থ্রেড থেকে সরান। সেই একটিমাত্র পরিবর্তন অনেক বাস্তব সমস্যা ঠিক করে।
নেটওয়ার্ক কলের জন্য রিট্রাই, টাইমআউট, এবং প্যারালাল কাজ
হ্যাপি পাথ কমই সমস্যা তৈরি করে। ঝামেলা আসে কলগুলো এখনই বন্ধ হয়ে যায় না, রিট্রাইগুলো আরও খারাপ করে, অথবা “প্যারালাল” কাজ আসলে প্যারালাল নয়। এই প্যাটার্নগুলো প্রায়শই দলকে Kotlin Coroutines vs RxJava পছন্দ করায় সিদ্ধান্ত নিতে প্রভাব ফেলে।
দ্রুত ব্যর্থতার জন্য টাইমআউট
Coroutines-এ আপনি যেকোন suspend কলের চারপাশে একটি হার্ড ক্যাপ রাখতে পারেন। টাইমআউট কল সাইটের কাছাকাছি রাখুন যাতে আপনি সঠিক UI বার্তা দেখাতে পারেন।
val user = withTimeout(5_000) {
api.getUser() // suspend
}
RxJava-তে আপনি স্ট্রিমে একটি timeout অপারেটর যোগ করেন। যখন টাইমআউট আচরণটি শেয়ার করা পাইপলাইনের অংশ হওয়া উচিত তখন সেটি উপকারী।
ক্ষতি ছাড়া রিট্রাই
কেবল তখনই রিট্রাই করুন যখন পুনরায় চেষ্টা করা নিরাপদ। একটি সহজ নিয়ম: idempotent অনুরোধগুলিতে (যেমন GET) আপনি বেশি ঢেড় করে রিট্রাই করতে পারেন, কিন্তু সাইড-ইফেক্ট তৈরি করা রিকোয়েস্টগুলিতে (যেমন "create order") সাবধান থাকুন। তবুও, সংখ্যা সীমাবদ্ধ করুন এবং ডিলে বা জিটার যুক্ত করুন।
ভাল ডিফল্ট গার্ডরেইলস:
- নেটওয়ার্ক টাইমআউট ও অস্থায়ী সার্ভার এররগুলোতে রিট্রাই করুন।
- ভ্যালিডেশন এরর (400s) বা অথ ফেইলিয়ারগুলিতে রিট্রাই করবেন না।
- রিট্রাই কেপ করুন (সাধারণত 2-3) এবং চূড়ান্ত ব্যর্থতা লগ করুন।
- ব্যাকঅফ ডিলে ব্যবহার করুন যাতে সার্ভারকে হাম করা না হয়।
RxJava-তে retryWhen আপনাকে "এই ত্রুটিগুলোতে, এই ডিলে দিয়ে রিট্রাই কর" এমন এক্সপ্রেশন করতে দেয়। Coroutines-এ Flow-এর retry এবং retryWhen আছে, আর সাধারণ suspend ফাংশনগুলোতে প্রায়ই আপনি ছোট একটি লুপ ও delay ব্যবহার করেন।
জটিল কোড ছাড়া প্যারালাল কল
Coroutines প্যারালাল কাজ সরাসরি করে: দুইটি রিকোয়েস্ট শুরু করুন, উভয়ের জন্য await করুন।
coroutineScope {
val profile = async { api.getProfile() }
val feed = async { api.getFeed() }
profile.await() to feed.await()
}
RxJava তখনই উজ্জ্বল যখন একাধিক সোর্স মিলানো চেইনের মূল উদ্দেশ্য। zip সাধারণত "উভয়ই অপেক্ষা কর" টুল, আর merge তখন কাজে লাগে যখন আপনি ফলাফল যত তাড়াতাড়ি আসে তা চান।
বড় বা দ্রুত স্ট্রিমের জন্য backpressure এখনো বিষয়। RxJava-র Flowable mature backpressure টুল রাখে। Coroutines Flow অনেক কেস ভালোভাবে হ্যান্ডল করে, কিন্তু ইভেন্টগুলো যদি আপনার UI বা ডাটাবেস লেখার চেয়ে দ্রুত আসে তবে আপনাকে buffering বা dropping পলিসি প্রয়োগ করতে হতে পারে।
ইন্টারঅপ এবং মাইগ্রেশন প্যাটার্ন (মিশ্র কোডবেস)
অধিকাংশ দল একেবারেই না বদলায়। একটি ব্যবহারিক Kotlin Coroutines vs RxJava মাইগ্রেশন অ্যাপকে স্থির রাখে যখন আপনি মডিউল-অনু অনুযায়ী স্থানান্তর করেন।
একটি Rx API-কে suspend ফাংশনে মোড়ান
যদি আপনার বিদ্যমান Single<T> বা Completable থাকে, সেটিকে ক্যান্সেলেশন সাপোর্ট সহ মোড়ান যাতে ক্যান্সেল করা coroutine Rx subscribe-কে 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 ক্যান্সেল হয়, কিন্তু নেটওয়ার্ক কল চালিয়ে যায় এবং পরে শেয়ার্ড স্টেট আপডেট করে।
Rx কলারে coroutine কোড প্রকাশ করা
মাইগ্রেশনের সময় কিছু লেয়ার এখনও 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 state, ডাটাবেস আপডেট, এবং paging-জাতীয় স্ট্রিম। এটি কম সরাসরি হতে পারে যদি আপনি প্রচুর হট স্ট্রিম, subjects, অ্যাডভান্সড ব্যাকপ্রেশার টিউনিং, বা আপনার টিম ইতিমধ্যে জানে এমন বড় সেটের কাস্টম অপারেটরগুলোর উপরে নির্ভর করেন।
মাইগ্রেশন কৌশল যা বিভ্রান্তি কম রাখে:
- লিফ মডিউলগুলো প্রথমে (নেটওয়ার্ক, স্টোরেজ) suspend API-তে কনভার্ট করুন।
- মডিউল সীমানায় ছোট অ্যাডাপ্টার যোগ করুন (Rx -> suspend, suspend -> Rx)।
- শুধুমাত্র তখন Flow দিয়ে Rx স্ট্রিমগুলোর প্রতিস্থাপন করুন যখন আপনি কনসিউমারদেরও নিয়ন্ত্রণ করেন।
- প্রতিটি ফিচার এরিয়ায় একটাই অ্যাসিঙ্ক স্টাইল রাখার চেষ্টা করুন।
- অ্যাডাপ্টারগুলো তখনই মুছুন যখন শেষ কলারও মাইগ্রেট করে ফেলে।
আপনি বাস্তবে ব্যবহার করবেন এমন টেস্টিং প্যাটার্ন
টাইমিং ও ক্যান্সেলেশন ইস্যুগুলোই সেই জায়গা যেখানে অ্যাসিঙ্ক বাগ লুকায়। ভালো অ্যাসিঙ্ক টেস্ট সময় নিঃসন্দেহ ও ফলাফল সহজে assert করার মতো করে দেয়। Kotlin Coroutines vs RxJava এখানে আলাদা অনুভূতি দেয়, যদিও উভয়ই ভালভাবে টেস্ট করা যায়।
Coroutines: runTest, TestDispatcher, এবং সময় নিয়ন্ত্রণ
Coroutine কোডের জন্য runTest একটি টেস্ট ডিসপ্যাচার-এর সাথে ব্যবহার করুন যাতে আপনার টেস্ট বাস্তব থ্রেড বা বাস্তব ডিলে-এ নির্ভর না করে। ভার্চুয়াল টাইম আপনাকে টাইমআউট, রিট্রাই, এবং 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 scope) ক্যান্সেল করুন এবং assert করুন আপনার fake API থামে বা আর কোনো স্টেট ইমিট হয় না।
RxJava: TestScheduler, TestObserver, নির্ধারিত সময়
Rx টেস্টগুলো সাধারণত TestScheduler টাইম কন্ট্রোলের জন্য ও TestObserver assertions এর জন্য একসাথে ব্যবহার করে।
@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
}
উভয় স্টাইলে error path টেস্ট করার সময়, exception টাইপের উপর নয় ম্যাপিংয়ের উপর ফোকাস করুন। আপনি 401, টাইমআউট, বা খারাপ রেসপন্সের পরে UI state-এ যা আশা করেন তা assert করুন।
একটি ছোট চেক সেট বেশিরভাগ রিগ্রেশন ঢাকা দেয়:
- Loading এবং চূড়ান্ত স্টেট (Success, Empty, Error)
- ক্যান্সেলেশন ক্লিনআপ (job cancelled, disposable disposed)
- ত্রুটি ম্যাপিং (সার্ভার কোড থেকে ব্যবহারকারী বার্তা)
- রিট্রাই-এর পর ডুপ্লিকেট ইমিশন নেই
- টাইম-ভিত্তিক লজিক ভার্চুয়াল টাইম ব্যবহার করে, বাস্তব ডিলে নয়
প্রোডাকশনে বাগ ঘটানোর সাধারণ ভুলগুলো
বেশিরভাগ প্রোডাকশন ইস্যু Kotlin Coroutines vs RxJava বেছে নেওয়ার কারণে নয়। এগুলো কয়েকটি অভ্যাস থেকে আসে যা কাজকে আপনি যতটা ভাবেন তার চেয়েও বেশি সময় চালায়, দ্বিগুণ চালায়, বা ভুল থ্রেডে UI স্পর্শ করে।
একটি সাধারণ লিক হল ভুল স্কোপে কাজ লঞ্চ করা। যদি আপনি একটি স্কোপ থেকে নেটওয়ার্ক কল শুরু করেন যা স্ক্রীন থেকে বেশি সময় বাঁচে (অথবা আপনি নিজে একটি স্কোপ তৈরি করে কখনো ক্যান্সেল না করেন), অনুরোধটি ব্যবহারকারী ছেড়ে যাওয়ার পরে শেষ হতে পারে এবং তারপরও স্টেট আপডেট করতে পারে। Coroutines-এ এটি প্রায়ই দীর্ঘ-জীবনকালের স্কোপ ব্যবহারের মতো দেখা যায়। RxJava-তে সাধারণত এটি মিস করা dispose।
আরেকটি ক্লাসিক সমস্যা হল "fire and forget"। গ্লোবাল স্কোপ ও ভুলে যাওয়া Disposables প্রথমে ঠিক মতোই মনে হতে পারে যতক্ষণ না কাজ জমে যায়। একটি চ্যাট স্ক্রীন যা প্রতিটি resume-এ রিফ্রেশ করে সহজেই কয়েকটি নেভিগেশনের পর একাধিক রিফ্রেশ জব নিয়ে ফেলতে পারে, প্রতিটিরা মেমরি ধরে রেখে নেটওয়ার্কের জন্য প্রতিযোগিতা করে।
রিট্রাইও ভুল করা সহজ। অনির্ধারিত রিট্রাই বা কোনো ডিলে ছাড়া রিট্রাই সার্ভারকে স্প্যাম করতে পারে ও ব্যাটারি খরচ বাড়ায়। বিশেষ করে বিপজ্জনক যখন ব্যর্থতা স্থায়ী, যেমন logout-এর পরে 401। রিট্রাই শর্তাধীন রাখুন, ব্যাকঅফ যোগ করুন, এবং এমন ত্রুটিতে থেমে যান যা recoverable নয়।
থ্রেডিং ত্রুটি এমন ক্র্যাশ সৃষ্টি করে যা পুনরায় তৈরি করা কঠিন। আপনি হয়ত মেইন থ্রেডে JSON পার্স করছেন বা ব্যাকগ্রাউন্ড থ্রেড থেকে UI আপডেট করছেন তার ওপর নির্ভর করে যেখানে dispatcher বা scheduler রাখেন।
দ্রুত চেকগুলো যা অধিকাংশ সমস্য ধরবে:
- কাজকে একটি লাইফসাইকেল ওনারের সঙ্গে বেঁধে দিন এবং ওনার শেষ হলে ক্যান্সেল করুন।
- ক্লিনআপ স্পষ্ট রাখুন: এক জায়গায় Jobs cancel বা Disposables clear করুন।
- রিট্রাইতে কঠোর সীমা রাখুন (কাউন্ট, ডিলে, এবং কোন ত্রুটি যোগ্য তা)।
- UI আপডেটের জন্য একটি নিয়ম প্রয়োগ করুন (শুধু মেইন থ্রেডে) কোড রিভিউ-তে।
- ব্যাকগ্রাউন্ড সিঙ্ককে একটি সিস্টেম হিসেবে বিবেচনা করুন, কোন র্যান্ডম ফাংশন কল নয়।
আপনি যদি generated Kotlin কোড থেকে Android অ্যাপ শিপ করেন (উদাহরণস্বরূপ, AppMaster থেকে), একই ফাঁকফোকর এখনো প্রযোজ্য। আপনাকে এখনও স্কোপ, ক্যান্সেলেশন, রিট্রাই সীমা, এবং থ্রেড নিয়মগুলোর জন্য স্পষ্ট কনভেনশন দরকার।
Coroutines, RxJava, অথবা উভয় বেছে নেওয়ার দ্রুত চেকলিস্ট
কাজের আকৃতি থেকে শুরু করুন। বেশিরভাগ নেটওয়ার্কিং কল একক-শট, কিন্তু অ্যাপে চলমান সিগনালও থাকতে পারে যেমন কানেক্টিভিটি, অথ স্টেট, বা লাইভ আপডেট। প্রথমে ভুল বিমূর্ততা বেছে নিলে পরে messy ক্যান্সেলেশন ও কঠিন ত্রুটি পাথ দেখা যায়।
দলকে সিদ্ধান্ত বোঝানোর সহজ উপায়:
- একবারের অনুরোধ (login, fetch profile):
suspendফাংশন পছন্দ করুন। - চলমান স্ট্রিম (ইভেন্ট, ডাটাবেস আপডেট):
Flowবা RxObservableপছন্দ করুন। - UI লাইফসাইকেল ক্যান্সেলেশন:
viewModelScopeবাlifecycleScope-এ coroutines সাধারণত ম্যানুয়াল disposables থেকে সহজ। - অগ্রগামী স্ট্রিম অপারেটর ও ব্যাকপ্রেশারে ব্যাপক নির্ভরতা: পুরনো কোডবেসে RxJava এখনও ভাল ফিট হতে পারে।
- জটিল রিট্রাই ও ত্রুটি ম্যাপিং: দল যে পদ্ধতিটা পড়তে পারে এবং বজায় রাখতে পারে তাতে যান।
একটি ব্যবহারিক নিয়ম: যদি একটি স্ক্রীন একটি অনুরোধ করে এবং একটি রেজাল্ট রেন্ডার করে, coroutines কোডকে সাধারণ ফাংশন কলের কাছাকাছি রাখে। যদি আপনি অনেক ইভেন্টের পাইপলাইন তৈরি করছেন (টাইপিং, debounce, পূর্বের অনুরোধ ক্যান্সেল, ফিল্টার মিলানো), RxJava বা Flow প্রাকৃতিকভাবে বেশি আরামদায়ক লাগতে পারে।
একটি বাস্তবিক নীতি: ধারাবাহিকতা শ্রেষ্ঠতাকে হারিয়ে দেয়। একই জায়গায় দুইটি ভালো প্যাটার্ন পাঁচটি "সেরা" প্যাটার্নের চেয়ে রক্ষণাবেক্ষণে সহজ।
উদাহরণ দৃশ্য: লগইন, প্রোফাইল ফেচ, ও ব্যাকগ্রাউন্ড সিঙ্ক
একটি সাধারণ প্রোডাকশন ফ্লো: ব্যবহারকারী Login ট্যাপ করে, আপনি একটি auth endpoint কল করেন, তারপর হোম স্ক্রীনের জন্য প্রোফাইল ফেচ করেন, এবং শেষে ব্যাকগ্রাউন্ড সিঙ্ক শুরু করেন। দৈনন্দিন রক্ষণাবেক্ষণে Kotlin Coroutines vs 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-তে একই ফ্লো চেইন হিসেবে থাকে। ক্যান্সেলেশন মানে dispose করা, সাধারণত একটি 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 vs RxJava নিজে ভুল নয়। একটি সংক্ষিপ্ত সিদ্ধান্ত নোট (এক পাতাও হতে পারে) রিভিউয়ে সময় বাঁচায় এবং আচরণকে পূর্বানুমেয় করে।
একটি সহজ বিভাজন দিয়ে শুরু করুন: এক-শট কাজ (একটি নেটওয়ার্ক কল যা একবারে রিটার্ন করে) বনাম স্ট্রিম (সময় ধরে আপডেট, যেমন ওয়েবসকেট ইভেন্ট, লোকেশন, বা ডেটাবেস পরিবর্তন)। প্রতিটি ক্ষেত্রে ডিফল্ট সিদ্ধান্ত নিন, এবং কবে ব্যতিক্রম অনুমোদিত তা নির্ধারণ করুন।
তারপর কিছু শেয়ার্ড হেল্পার যোগ করুন যাতে প্রতিটি ফিচার নেটওয়ার্ক খারাপ হলে একইভাবে আচরণ করে:
- এক জায়গায় ত্রুটি ম্যাপিং (HTTP কোড, টাইমআউট, অফলাইন) যাতে UI বুঝতে পারে
- নেটওয়ার্ক কলের জন্য ডিফল্ট টাইমআউট মান, লম্বা অপারেশনের জন্য ওভাররাইড করার উপায়
- কোনগুলো নিরাপদে রিট্রাই করা যায় তা নির্দেশ করে একটি রিট্রাই পলিসি
- ক্যান্সেলেশন নিয়ম: ব্যবহারকারী স্ক্রীন ছেড়ে গেলে কি বন্ধ হবে, কি চালু থাকতে পারবে
- সাপোর্টিং লগিং নীতিমালা যা সাপোর্ট করে কিন্তু সংবেদনশীল ডেটা লিক নয়
টেস্টিং কনভেনশনও ততটাই গুরুত্বপূর্ণ। একভাবে চুক্তি করুন যাতে টেস্টগুলো বাস্তব সময় বা থ্রেডে নির্ভর না করে। Coroutines-এর জন্য সাধারণত একটি টেস্ট ডিসপ্যাচার ও স্ট্রাকচার্ড স্কোপ; RxJava-র জন্য TestScheduler ও স্পষ্ট disposal। যে কোনো পদ্ধতিতেই দ্রুত, নির্ধারিত টেস্ট লক্ষ্য করুন যাতে কোন স্লিপ হয় না।
আপনি যদি দ্রুত এগোতে চান, AppMaster (appmaster.io) একটি অপশন হতে পারে ব্যাকএন্ড API ও Kotlin-ভিত্তিক মোবাইল অ্যাপ জেনারেট করে শুঁকো না লিখেই। জেনারেট করা কোড থাকলেও একই প্রোডাকশন কনভেনশনগুলো—ক্যান্সেলেশন, ত্রুটি, রিট্রাই, এবং টেস্টিং—নেটওয়ার্কিং আচরণকে পূর্বানুমেয় রাখে।
প্রশ্নোত্তর
Default to suspend for one request that returns once, like login or fetching a profile. Use Flow (or Rx streams) when values change over time, like websocket messages, connectivity, or database updates.
Yes, but only if your HTTP client is cancellable. Coroutines stop the coroutine when the scope is cancelled, but the underlying HTTP call must also support cancellation or the request may continue in the background.
Tie work to a lifecycle scope, like viewModelScope, so it cancels when the screen logic ends. Avoid launching in long-lived or global scopes unless the work is truly app-wide.
In coroutines, failures usually throw and you handle them with try/catch close to where you can map them to UI state. In RxJava, errors travel through the stream, so keep the error path explicit and avoid operators that silently turn failures into default values.
Use exceptions for unexpected failures like timeouts, 500s, or parsing issues. Use typed error data when the UI needs a specific response like “wrong password” or “email already used,” so you don’t rely on string matching.
Apply a timeout where you can show the right UI message, like near the call site. In coroutines, withTimeout is straightforward for suspend calls; in RxJava, a timeout operator makes the timeout part of the chain.
Retry only when it’s safe, usually for idempotent requests like GET, and cap it at a small number like 2–3. Don’t retry on validation errors or auth failures, and add delays so you don’t hammer the server or drain battery.
Coroutines use Dispatchers and usually start on Main for UI, then switch to IO or Default for expensive work. RxJava uses subscribeOn for where upstream runs and observeOn for where you consume results; keep one final switch to main right before rendering to avoid surprises.
Yes, but keep the boundary small and cancellation-aware. Wrap Rx into suspend with a cancellable adapter that disposes on coroutine cancellation, and only expose suspend work to Rx callers in a limited, well-documented bridge.
Use virtual time so tests don’t sleep or depend on real threads. For coroutines, runTest with a test dispatcher lets you control delays and cancellation; for RxJava, use TestScheduler and assert no emissions after dispose().


