المقارنة بين Kotlin Coroutines و RxJava للشبكات والعمل في الخلفية
Kotlin Coroutines مقابل RxJava: قارن الإلغاء، معالجة الأخطاء، وأنماط الاختبار للشبكات والعمل في الخلفية في تطبيقات Android الحقيقية.

لماذا يهم هذا الاختيار في شبكات الإنتاج
الشبكات والعمل في الخلفية في تطبيق Android حقيقي أكثر من مجرد استدعاء واجهة برمجة تطبيقات واحدة. يشمل ذلك تسجيل الدخول وتجديد الرموز، شاشات يمكن تدويرها أثناء التحميل، المزامنة بعد مغادرة المستخدم للشاشة، رفع الصور، والعمل الدوري الذي لا يجب أن يستنزف البطارية.
الأخطاء التي تؤثر بشدة عادة ليست أخطاء تركيبية. تظهر عندما يستمر العمل غير المتزامن بعد انتهاء واجهة المستخدم (تسريبات)، عندما يوقف الإلغاء الواجهة لكن لا يوقف الطلب الفعلي (حركة مرور مهدرة ومؤشرات تحميل متوقفة)، عندما تتضاعف المحاولات وتضاعف الطلبات (حدود معدل، حظر)، أو عندما تتعامل الطبقات المختلفة مع الأخطاء بطرق مختلفة فلا يمكن لأحد التنبؤ بما سيراه المستخدم.
القرار بين Kotlin Coroutines و RxJava يؤثر على الاعتمادية اليومية:
- كيف تصمم العمل (مكالمات لمرة واحدة مقابل تدفقات)
- كيف ينتشر الإلغاء
- كيف تُمثّل الأخطاء وتُعرض على الواجهة
- كيف تتحكم بالخيوط للشبكة والقرص والواجهة
- مدى اختبار توقيتات وإعادة المحاولة وحالات الحافة
الأنماط أدناه تركز على ما يميل إلى التعطل تحت التحميل أو على شبكات بطيئة: الإلغاء، معالجة الأخطاء، المحاولات والمهلات، وعادات الاختبار التي تمنع الانحداسات. الأمثلة قصيرة وعملية.
النماذج الذهنية الأساسية: استدعاءات suspend, التدفقات، و Flow
الفرق الرئيسي بين Kotlin Coroutines و RxJava هو شكل العمل الذي تصممه.
دالة suspend تمثل عملية لمرة واحدة. تُرجع قيمة واحدة أو ترمي فشلًا واحدًا. هذا يطابق معظم استدعاءات الشبكة: جلب الملف الشخصي، تحديث الإعدادات، رفع صورة. الكود المستدعي يُقرأ من الأعلى إلى الأسفل، وهذا يبقى سهل المسح حتى بعد إضافة تسجيل، تخزين مؤقت، وتفرعات.
RxJava يبدأ بالسؤال فيما إذا كنت تتعامل مع قيمة واحدة أو عدة قيم عبر الزمن. Single هو نتيجة لمرة واحدة (نجاح أو خطأ). Observable (أو Flowable) هو تيار يمكنه إصدار قيم متعددة ثم إكمال أو الفشل. هذا يناسب ميزات تشبه الأحداث حقًا: أحداث تغيير النص، رسائل websocket، أو الاستطلاع.
Flow هو الطريقة الملائمة للكوروتين لتمثيل تيار. اعتبره النسخة “التيارية” للكوروتين، مع إلغاء مُنظَّم وتوافق مباشر مع واجهات API التي تدعم suspend.
قاعدة إبهام سريعة:
- استخدم
suspendلطلب واحد واستجابة واحدة. - استخدم
Flowللقيم التي تتغير عبر الزمن. - استخدم RxJava عندما يعتمد تطبيقك بشدة على المشغلات والمركبات المعقدة للتيارات.
مع نمو الميزات، عادة ما يتدهور قابلية القراءة أولًا عندما تجبر نموذج تيار على استدعاء لمرة واحدة، أو تحاول معاملة أحداث جارية كقيمة عائدة واحدة. طابق التجريد مع الواقع أولًا، ثم أبنِ قواعد للفريق حوله.
الإلغاء في الممارسة (مع أمثلة قصيرة للكود)
الإلغاء هو المكان الذي يجعل فيه الكود غير المتزامن إما يشعر بالأمان، أو يتحول إلى تحطمات عشوائية وطلبات ضائعة. الهدف بسيط: عندما يغادر المستخدم الشاشة، يجب أن يتوقف أي عمل بدأ لهذه الشاشة.
مع Kotlin Coroutines، الإلغاء مدمج في النموذج. Job يمثل العمل، ومع الـ structured concurrency عادة لا تمرر الـ jobs حول. تبدأ العمل داخل نطاق (مثل 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
}
}
تفصيلان عمليان مهمان:
- استدعِ شبكات عبر عميل قابل للإلغاء. وإلّا، قد يتوقف الكوروتين لكن يستمر طلب HTTP في الخلفية.
- استخدم
withTimeout(أوwithTimeoutOrNull) للطلبات التي لا يجب أن تتوقف فيها المدة طويلاً.
RxJava تستخدم التخلص الصريح. تحتفظ بـ 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 هو كيف تسافر الأخطاء. الكوروتين تجعل الفشل يبدو ككود عادي: استدعاء suspend يرمي، والمتصل يقرر ما يفعل. Rx يدفع الأخطاء عبر التيار، وهذا قوي، لكنه يسهل إخفاء المشاكل إن لم تكن حريصًا.
استخدم الاستثناءات للفشل غير المتوقع (مهلات، 500s، أخطاء تحويل). مثل أخطاء واجهة المستخدم عندما تحتاج إلى رد محدد (كلمة مرور خاطئة، "البريد مستخدم بالفعل") نمذجتها كبيانات وتضمّنها في نموذج النطاق الخاص بك.
نموذج كوروتين بسيط يحتفظ بتتبع الستاك ويبقى قابلًا للقراءة:
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، اجعل مسار الخطأ واضحًا. استخدم onErrorReturn فقط للبدائل الآمنة. فضّل onErrorResumeNext عندما تحتاج لتبديل المصدر (مثلاً إلى بيانات مخبأة). للمحاولات، اجعل القواعد ضيقة باستخدام retryWhen حتى لا تعيد المحاولة على حالة مثل "كلمة مرور خاطئة".
مجموعة عادات تمنع ابتلاع الأخطاء:
- سجّل أو أبلغ عن الخطأ مرة واحدة، بالقرب من المكان الذي لديك فيه السياق.
- احتفظ بالاستثناء الأصلي كـ
causeعند التغليف. - تجنب بدائل الالتقاط العام التي تحوّل كل خطأ إلى قيمة افتراضية.
- اجعل أخطاء الواجهة بيانات مُصنَّفة، لا سلاسل نصية.
أساسيات الخيوط: Dispatchers مقابل Schedulers
العديد من أخطاء اللا-تزامن تعود إلى الخيوط: تنفيذ عمل ثقيل على الخيط الرئيسي، أو لمس الواجهة من خيط خلفي. الاختلاف بين Kotlin Coroutines و RxJava هو بشكل أساسي في كيف تعبّر عن تبديلات الخيط.
مع الكوروتين، غالبًا تبدأ على الخيط الرئيسي لأعمال الواجهة، ثم تقفز إلى Dispatcher الخلفي للأجزاء المكلفة. الاختيارات الشائعة:
Dispatchers.Mainلتحديثات الواجهةDispatchers.IOللـ I/O المحظور مثل الشبكة والقرصDispatchers.Defaultللعمل الحسابي مثل تحليل JSON، الفرز، التشفير
نمط مباشر: جلب البيانات، تحليل خارج الخيط الرئيسي، ثم العرض.
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) }
)
قاعدة لتجنّب المفاجآت: احتفظ بعمل الواجهة في مكان واحد. في الكوروتين، قم بتعيين أو جمع حالة الواجهة على Dispatchers.Main. في RxJava، ضع observeOn(main) أخيرًا قبل العرض، ولا تفرّق observeOn إضافي إلا إذا كنت في حاجة حقيقية.
إذا تعطّلت الشاشة، انقل التحليل والـ mapping خارج الخيط الرئيسي أولًا. هذا التغيير الواحد يصلح الكثير من المشاكل الواقعية.
المحاولات، المهلات، والعمل المتوازي لاستدعاءات الشبكة
المسار السعيد نادرًا ما يكون المشكلة. المشاكل تأتي من طلبات تتوقف، محاولات تزيد الطين بلة، أو عمل "متوازي" ليس فعلاً متوازيًا. هذه الأنماط غالبًا ما تحدد تفضيل الفرق بين Kotlin Coroutines و RxJava.
مهلات تفشل بسرعة
مع الكوروتين، يمكنك وضع حد زمني حول أي استدعاء suspend. احتفظ بالمهلة قريبة من موقع الاستدعاء حتى تتمكن من عرض رسالة الواجهة المناسبة.
val user = withTimeout(5_000) {
api.getUser() // suspend
}
في RxJava، تضيف عامل timeout إلى التيار. هذا مفيد عندما يجب أن يكون سلوك المهلة جزءًا من أنبوب مشترك.
محاولات بدون ضرر
أعد المحاولة فقط عندما تكون إعادة المحاولة آمنة. قاعدة بسيطة: أعد المحاولة للطلبات idempotent (مثل GET) بحرية أكثر مما تفعل للطلبات التي تُنشئ آثارًا جانبية (مثل "إنشاء طلب"). حتى في هذه الحالة، حدّ العدد وأضف تأخيرًا أو jitter.
حواجز افتراضية جيدة:
- أعد المحاولة على مهلات الشبكة وأخطاء الخادم المؤقتة.
- لا تعيد المحاولة على أخطاء التحقق (400s) أو فشل المصادقة.
- حدّ المحاولات (غالبًا 2-3) وسجّل الفشل النهائي.
- استخدم تأخيرات متزايدة حتى لا تهاجم الخادم.
في RxJava، يسمح retryWhen بالتعبير عن "أعد المحاولة فقط لهذه الأخطاء، بهذا التأخير". في الكوروتين، يحتوي Flow على retry و retryWhen، بينما دوال suspend عادة ما تستخدم حلقة صغيرة مع delay.
استدعاءات متوازية دون تشابك الكود
تجعل الكوروتين العمل المتوازٍ مباشرًا: ابدأ طلبين وانتظر الاثنين.
coroutineScope {
val profile = async { api.getProfile() }
val feed = async { api.getFeed() }
profile.await() to feed.await()
}
تتألق RxJava عندما يكون دمج مصادر متعددة هو الهدف الأساسي للسلسلة. zip الأداة المعتادة لـ"الانتظار لكليهما"، وmerge مفيد عندما تريد النتائج فور وصولها.
للتدفقات الكبيرة أو السريعة، لا يزال الضغط الخلفي matter. لدى RxJava أدوات ناضجة للضغط الخلفي عبر Flowable. يتعامل Flow في الكوروتين مع العديد من الحالات جيدًا، لكن قد تحتاج لسياسات buffering أو dropping إذا كانت الأحداث تفوق قدرة الواجهة أو قاعدة البيانات على المعالجة.
التوافق وأنماط الترحيل (قواعد شفرة مختلطة)
معظم الفرق لا تتحول بين ليلة وضحاها. ترحيل عملي بين Kotlin Coroutines و RxJava يحافظ على استقرار التطبيق أثناء انتقالك وحدة بوحدة.
غلف API من Rx في دالة suspend
إذا كان لديك Single<T> أو Completable موجود، غلفه بدعم الإلغاء حتى يُلغي الكوروتين اشتراك Rx عند إلغائه.
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() }
}
هذا يتجنّب نمط فشل شائع: يغادر المستخدم الشاشة، يُلغى الكوروتين، لكن طلب الشبكة يستمر ويحدّث حالة مشتركة لاحقًا.
افتح كود الكوروتين لنداءات Rx
أثناء الترحيل، بعض الطبقات ستظل تتوقع أنواع Rx. غلف العمل القائم على suspend داخل Single.fromCallable وادخل blocking فقط على خيط خلفي.
fun loadProfileRx(api: Api): Single<Profile> =
Single.fromCallable {
runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
}
اجعل هذا الحد الصغير موثقًا. للكود الجديد، فضّل استدعاء API الـ suspend مباشرة من نطاق كوروتين.
أين يناسب Flow وأين لا
يمكن أن يحل Flow محل العديد من حالات Observable: حالة الواجهة، تحديثات قاعدة البيانات، وتدفقات تشبه الـ paging. قد يكون أقل مباشرة إذا كنت تعتمد بشدة على التيارات الساخنة، subjects، ضبط خلفي متقدم، أو مجموعة كبيرة من المشغلات المخصصة التي يعرفها فريقك جيدًا.
استراتيجية ترحيل تقلل الالتباس:
- حوّل الوحدات الطرفية أولًا (الشبكة، التخزين) إلى واجهات
suspend. - أضف محولات صغيرة على حدود الوحدات (Rx إلى suspend، suspend إلى Rx).
- استبدل تيارات Rx بـ Flow فقط عندما تتحكم أيضاً في المستهلكين.
- احتفظ بأسلوب غير متزامن واحد لكل مجال ميزة.
- احذف المحولات عندما يهاجر آخر مستدعي.
أنماط الاختبار التي ستستخدمها فعليًا
قضايا التوقيت والإلغاء هي المكان الذي تختبئ فيه أخطاء اللا-تزامن. اختبارات جيدة تجعل الزمن حتميًا والنتائج سهلة التأكيد. هذا مجال آخر حيث يشعر الفرق باختلاف بين Kotlin Coroutines و RxJava، مع أن كلاهما يُختبَر جيدًا.
الكوروتين: runTest، TestDispatcher، والتحكم في الزمن
لكود الكوروتين، فضّل runTest مع موزع اختبار حتى لا تعتمد الاختبارات على الخيوط الحقيقية أو التأخيرات الحقيقية. يتيح الزمن الافتراضي تحفيز المهلات والمحاولات ونوافذ debounce بدون استخدام sleep.
@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()
}
لاختبار الإلغاء، ألغِ Job الجامع (أو نطاق الأب) وتحقق أن API الوهمي يتوقف أو لا تُبعث حالات إضافية.
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
}
عند اختبار مسارات الخطأ في أي نمط، ركّز على التحويل (mapping)، لا على نوع الاستثناء. أكد حالة الواجهة المتوقعة بعد 401، مهلة، أو استجابة معطوبة.
مجموعة صغيرة من الفحوصات تغطي معظم الانحدارات:
- حالات التحميل والحالات النهائية (نجاح، فارغ، خطأ)
- تنظيف الإلغاء (Job ملغى، disposable مُنظف)
- تحويل الأخطاء (أكواد الخادم إلى رسائل للمستخدم)
- عدم إرسال انبعاثات مكررة بعد المحاولات
- منطق معتمَد على الزمن باستخدام زمن افتراضي، لا تأخيرات حقيقية
أخطاء شائعة تسبب مشاكل في الإنتاج
معظم مشاكل الإنتاج لا تحدث لأنك اخترت Kotlin Coroutines أو RxJava. بل تحدث من عادات تجعل العمل يستمر أطول مما تظن، يعمل مرتين، أو يلمس الواجهة في وقت خاطئ.
تسريب شائع هو إطلاق عمل في نطاق خاطئ. إذا بدأت طلب شبكة من نطاق يبقى بعد انتهاء الشاشة (أو أنشأت نطاقك الخاص ولم تُلغِه)، يمكن أن ينتهي الطلب بعد مغادرة المستخدم ولا يزال يحاول تحديث الحالة. في الكوروتين، يظهر هذا غالبًا كمجال طويل العمر. في RxJava، عادة يكون Disposable منسي.
كلاسيكي آخر هو "أطلق وانسَ". النطاقات العالمية والـ Disposables المنسية تبدو بخير حتى تتراكم الأعمال. شاشة دردشة تُحدّث عند كل resume يمكن أن ينتهي بها الأمر بعد عدة تنقّلات بعدة مهام تحديث تعمل في نفس الوقت، كل منها يحتفظ بالذاكرة ويتنافس على الشبكة.
إعادة المحاولة أيضًا سهلة الخطأ. المحاولات غير المحدودة، أو بدون تأخير، يمكن أن تطرح طلبات على الخادم وتستنزف البطارية. الأمر خطير خاصة عندما يكون الفشل دائمًا، مثل 401 بعد تسجيل الخروج. اجعل إعادة المحاولة شرطية، أضف backoff، وتوقف عندما يكون الخطأ غير قابل للاسترداد.
أخطاء الخيوط تتسبب في تحطمات يصعب إعادة إنتاجها. قد تحلل JSON على الخيط الرئيسي أو تحدّث الواجهة من خيط خلفي اعتمادًا على أين وضعت Dispatcher أو Scheduler.
فحوصات سريعة تلتقط معظم هذه القضايا:
- اربط العمل بمالك دورة حياة وألغِه عند انتهاء ذلك المالك.
- اجعل التنظيف واضحًا: إلغاء الـ Jobs أو تنظيف الـ Disposables في مكان واحد.
- ضع حدودًا صارمة للمحاولات (عدد، تأخير، وأنواع الأخطاء المؤهلة).
- فرض قاعدة واحدة لتحديثات الواجهة (الخيط الرئيسي فقط) في ملاحظات الكود.
- عامل المزامنة الخلفية كنظام له قيود، لا كاستدعاء دالة عشوائي.
إذا كنت تُصدر تطبيقات Android من كود Kotlin مُولَّد (على سبيل المثال من AppMaster (appmaster.io))، فالمزالق نفسها لا تزال تنطبق. ما زلت بحاجة إلى قواعد واضحة للنطاقات، الإلغاء، حدود المحاولات، وقواعد الخيوط.
قائمة سريعة لاختيار Coroutines أو RxJava أو كلاهما
ابدأ بشكل العمل. معظم استدعاءات الشبكة هي لمرة واحدة، لكن التطبيقات لديها إشارات جارية مثل الاتصال، حالة المصادقة، أو التحديثات الحية. اختيار التجريد الخاطئ مبكرًا يظهر لاحقًا كإلغاء فوضوي ومسارات أخطاء يصعب قراءتها.
طريقة بسيطة لاتخاذ القرار (ولشرحها للفريق):
- طلب لمرة واحدة (تسجيل الدخول، جلب الملف): فضّل دالة
suspend. - تيار جارٍ (أحداث، تحديثات قاعدة البيانات): فضّل
Flowأو RxObservable. - إلغاء مرتبط بدورة حياة الواجهة: الكوروتين في
viewModelScopeأوlifecycleScopeأبسط عادة من الـ disposables اليدوية. - اعتماد كبير على مشغلات تيار متقدمة والضغط الخلفي: RxJava قد تظل أنسب، خاصة في قواعد الشفرة الأقدم.
- محاولات معقدة وتحويل الأخطاء: اختر النمط الذي يستطيع فريقك الحفاظ على قابليته للقراءة.
قاعدة عملية: إذا كانت شاشة واحدة تقوم بطلب واحد وتعرض نتيجة واحدة، الكوروتين تبقي الكود قريبًا من استدعاء دالة عادية. إذا كنت تبني خط أنابيب من أحداث كثيرة (كتابة، debounce، إلغاء الطلبات السابقة، دمج فلاتر)، فإن RxJava أو Flow غالبًا ما يشعران بأنهما أكثر طبيعية.
الاتساق أفضل من الكمال. نمطان جيدان مستخدَمان في كل مكان أسهل من خمسة "أفضل" تُستخدم بشكل غير متناسق.
سيناريو نموذجي: تسجيل الدخول، جلب الملف، والمزامنة الخلفية
تدفق شائع في الإنتاج: يضغط المستخدم تسجيل الدخول، تستدعي نقطة مصادقة، ثم تجلب الملف للشاشة الرئيسية، وأخيرًا تبدأ مزامنة خلفية. هنا تشعر الفروق بين Kotlin Coroutines و RxJava في الصيانة اليومية.
نسخة الكوروتين (تسلسلية + قابلة للإلغاء)
مع الكوروتين، شكل "افعل هذا، ثم ذاك" طبيعي. إذا أغلق المستخدم الشاشة، إلغاء النطاق يوقف العمل الجاري.
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 أو RxJava "خاطئ". مذكرة قرار قصيرة (حتى صفحة واحدة) توفّر وقتًا في المراجعات وتجعل السلوك متوقعًا.
ابدأ بتقسيم واضح: عمل لمرة واحدة مقابل تدفقات. قرّر الافتراضي لكلٍ منهما وحدد متى يُسمح بالاستثناءات.
ثم أضف مجموعة أدوات صغيرة مشتركة حتى يتصرف كل ميزة بنفس الطريقة عند سوء تصرّف الشبكة:
- مكان واحد لتحويل الأخطاء (أكواد HTTP، مهلات، عدم الاتصال) إلى فشل بمستوى التطبيق تفهمه الواجهة
- قيم مهلة افتراضية لاستدعاءات الشبكة، مع طريقة واضحة للاستثناء للعمليات الطويلة
- سياسة إعادة محاولة توضح ما الآمن لإعادة محاولته (مثلاً GET مقابل POST)
- قاعدة إلغاء: ما يتوقف عند مغادرة المستخدم للشاشة، وما يُسمح له بالاستمرار
- قواعد تسجيل تساعد الدعم دون تسريب بيانات حساسة
اتفاقيات الاختبار مهمة بنفس القدر. اتفق على نهج قياسي حتى لا تعتمد الاختبارات على الزمن الحقيقي أو الخيوط الحقيقية. للكوروتين عادة يعني ذلك موزع اختبار ونطاقات منظمة. لـ RxJava عادة يعني ذلك test schedulers وتنظيف صريح. في كلتا الحالتين، اهدف لاختبارات سريعة وحتمية بدون sleeps.
إذا أردت التنقل أسرع، AppMaster (appmaster.io) خيار لبناء واجهات خلفية وتطبيقات Kotlin مُولدة بدون كتابة كل شيء من الصفر. حتى مع الشفرة المولدة، الاتفاقيات الإنتاجية نفسها حول الإلغاء، الأخطاء، حدود المحاولات، والاختبار هي ما يجعل سلوك الشبكات متوقعًا.
الأسئلة الشائعة
الافتراضي هو استخدام suspend لطلب واحد يرجع مرة واحدة، مثل تسجيل الدخول أو جلب الملف الشخصي. استخدم Flow (أو تيارات Rx) عندما تتغير القيم عبر الزمن، مثل رسائل websocket، حالة الاتصال، أو تحديثات قاعدة البيانات.
نعم — بشرط أن يكون عميل HTTP قابلًا للإلغاء. تقوم الكوروتين بإيقاف الـ coroutine عند إلغاء النطاق، لكن يجب أن يدعم عميل الشبكة إلغاء الطلب كي يتوقف الطلب نفسه بدلاً من الاستمرار في الخلفية.
اربط العمل بنطاق دورة حياة مثل viewModelScope حتى يُلغى عندما ينتهي منطق الشاشة. تجنّب إطلاق عمليات في نطاقات طويلة العمر أو عالمية ما لم تكن العملية فعلاً على مستوى التطبيق بأكمله.
في الكوروتين، تفشل الاستدعاءات عادة عبر رمي استثناء وتتعامل معها بـ try/catch قرب المكان الذي يمكن فيه تحويلها لحالة واجهة المستخدم. في RxJava، تنتقل الأخطاء عبر التيار، فاجعل مسار الخطأ واضحًا وتجنّب مشغلات تُحوّل الفشل إلى قيم افتراضية بصمت.
استخدم الاستثناءات للفشل غير المتوقّع مثل انتهاء المهلة، أخطاء الخادم 500، أو مشاكل التهيئة. امثل أخطاء الواجهة كبيانات مصنفة عندما تحتاج الواجهة إلى رد محدد مثل “كلمة مرور خاطئة” أو “البريد مستخدم بالفعل”، بدل الاعتماد على تطابق نصي.
ضع المهلة بالقرب من موقع الاستدعاء حتى يمكنك إظهار الرسالة المناسبة. في الكوروتين استخدم withTimeout لاستدعاءات suspend، وفي RxJava استخدم عامل timeout داخل السلسلة.
أعد المحاولة فقط عندما تكون آمنة — عادةً للطلبات idempotent مثل GET — وحدد عدد محاولات محدود (مثلاً 2–3)، وتجنّب إعادة المحاولة على أخطاء التحقق أو المصادقة. أضف تأخيرًا أو jitter لتجنب هجوم على الخادم أو استنزاف البطارية.
الخطأ الشائع هو تنفيذ عمل باهظ على الخيط الرئيسي أو تحديث الواجهة من خيط خلفي. في الكوروتين استخدم Dispatchers المناسبة (Main للواجهة، IO للـ I/O، Default للعمل الحسابي). في RxJava استخدم subscribeOn للمصدر وobserveOn للمكان الذي تستهلك فيه النتائج، وضع تحويل نهائي إلى main قبل العرض.
نعم، لكن احتفظ بحدود صغيرة وواضحة. غلف Rx في suspend بدِرع قابل للإلغاء (فيُلغى الاشتراك عند إلغاء الكوروتين)، وغيّر من suspend إلى Rx فقط عند الحاجة ووضح الحدود في التوثيق.
استخدم زمنًا افتراضيًا للاختبارات بدل الانتظار الحقيقي. في الكوروتين استخدم runTest وموزع اختبار للتحكم في التأخيرات والإلغاء؛ في RxJava استخدم TestScheduler وتحقق أنه لا توجد إصدارات بعد dispose().


