الروتين المشترك هو نمط تصميم للتنفيذ المتزامن يمكنك استخدامه على Android لتبسيط الرمز الذي يتم تنفيذه بشكل غير متزامن. تمت إضافة الروتينات الفرعية إلى Kotlin في الإصدار 1.3، وهي تستند إلى مفاهيم راسخة من لغات أخرى.
على نظام التشغيل Android، تساعد الروتينات المشتركة في إدارة المهام التي تستغرق وقتًا طويلاً والتي قد تحظر سلسلة التعليمات الرئيسية وتتسبب في عدم استجابة تطبيقك. أفاد أكثر من% 50 من المطوّرين المحترفين الذين يستخدمون إجراءات فرعية بأنّهم لاحظوا زيادة في الإنتاجية. يوضّح هذا الموضوع كيف يمكنك استخدام إجراءات Kotlin الفرعية لمعالجة هذه المشاكل، ما يتيح لك كتابة رمز تطبيق أكثر وضوحًا واختصارًا.
الميزات
الكوروتينات هي الحلّ الذي ننصح به للبرمجة غير المتزامنة على Android. تشمل الميزات الجديرة بالذكر ما يلي:
- خفيفة الوزن: يمكنك تشغيل العديد من الروتينات الفرعية على سلسلة محادثات واحدة بسبب توفّر ميزة التعليق، التي لا تحظر سلسلة المحادثات التي يتم تشغيل الروتين الفرعي عليها. يوفّر التعليق المؤقت الذاكرة مقارنةً بالحظر مع إتاحة العديد من العمليات المتزامنة.
- تسريب أقل للذاكرة: استخدِم التزامن المنظَّم لتنفيذ العمليات ضمن نطاق.
- إمكانية إلغاء مدمجة: يتم نشر الإلغاء تلقائيًا من خلال التسلسل الهرمي للكوروتين قيد التشغيل.
- عمليات الدمج مع Jetpack: تتضمّن العديد من مكتبات Jetpack إضافات توفّر إمكانية استخدام كاملة لأنماط "كوروتين". توفّر بعض المكتبات أيضًا نطاق روتين فرعي خاصًا بها يمكنك استخدامه للتزامن المنظَّم.
نظرة عامة على الأمثلة
استنادًا إلى دليل تصميم التطبيقات، تقدّم الأمثلة الواردة في هذا الموضوع طلب شبكة وتعرض النتيجة في سلسلة التعليمات الرئيسية، حيث يمكن للتطبيق بعد ذلك عرض النتيجة للمستخدم.
على وجه التحديد، يستدعي مكوّن ViewModel
Architecture طبقة المستودع في سلسلة التعليمات الرئيسية لتفعيل طلب الشبكة. يستعرض هذا الدليل حلولاً مختلفة تستخدم إجراءات روتينية مشتركة للحفاظ على عدم حظر سلسلة التعليمات الرئيسية.
تتضمّن ViewModel مجموعة من إضافات KTX التي تعمل مباشرةً مع الروتينات المشتركة. هذه الإضافات هي
مكتبة lifecycle-viewmodel-ktx ويتم استخدامها
في هذا الدليل.
معلومات التبعية
لاستخدام كوروتين في مشروع Android، أضِف الاعتمادية التالية إلى ملف build.gradle في تطبيقك:
Groovy
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
التنفيذ في سلسلة محادثات في الخلفية
عند إجراء طلب شبكة على سلسلة التعليمات الرئيسية، سيؤدي ذلك إلى انتظارها أو حظرها إلى أن تتلقّى ردًا. وبما أنّ سلسلة المحادثات محظورة، لا يمكن لنظام التشغيل استدعاء onDraw()، ما يؤدي إلى توقّف تطبيقك عن الاستجابة وظهور مربّع حوار "التطبيق لا يستجيب" (ANR). لتقديم تجربة أفضل للمستخدم، لننفّذ هذه العملية في سلسلة محادثات في الخلفية.
أولاً، لنلقِ نظرة على الفئة Repository ونرى كيف يتم تنفيذ طلب الشبكة:
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // Function that makes the network request, blocking the current thread fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { val url = URL(loginUrl) (url.openConnection() as? HttpURLConnection)?.run { requestMethod = "POST" setRequestProperty("Content-Type", "application/json; utf-8") setRequestProperty("Accept", "application/json") doOutput = true outputStream.write(jsonBody.toByteArray()) return Result.Success(responseParser.parse(inputStream)) } return Result.Error(Exception("Cannot open HttpURLConnection")) } }
تكون makeLoginRequest متزامنة وتحظر سلسلة التعليمات التي تستدعيها. لنمذجة استجابة طلب الشبكة، لدينا فئة Result الخاصة بنا.
يؤدي ViewModel إلى تشغيل طلب الشبكة عندما ينقر المستخدم، مثلاً، على زر:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } }
باستخدام الرمز السابق، يحظر LoginViewModel سلسلة واجهة المستخدم عند تقديم طلب الشبكة. أبسط حلّ لنقل التنفيذ
من سلسلة التعليمات الرئيسية هو إنشاء روتين فرعي جديد وتنفيذ طلب الشبكة
في سلسلة تعليمات الإدخال/الإخراج:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine to move the execution off the UI thread viewModelScope.launch(Dispatchers.IO) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } } }
لنحلّل رمز إجراءات coroutines في الدالة login:
viewModelScopeهوCoroutineScopeمحدّد مسبقًا ومضمّن مع إضافاتViewModelKTX. يُرجى العِلم أنّه يجب تشغيل جميع الروتينات الفرعية في نطاق. يديرCoroutineScopeروتينًا فرعيًا واحدًا أو أكثر من الروتينات الفرعية ذات الصلة.-
launchهي دالة تنشئ روتينًا فرعيًا وتوزّع تنفيذ نص الدالة على أداة التوزيع المناسبة. - يشير
Dispatchers.IOإلى أنّه يجب تنفيذ هذا الكوروتين على سلسلة تعليمات مخصّصة لعمليات وحدات الإدخال والإخراج.
يتم تنفيذ الدالة login على النحو التالي:
- يستدعي التطبيق الدالة
loginمن الطبقةViewفي سلسلة التعليمات الرئيسية. - تنشئ
launchروتينًا فرعيًا جديدًا، ويتم تنفيذ طلب الشبكة بشكل مستقل في سلسلة محادثات مخصّصة لعمليات الإدخال والإخراج. - أثناء تنفيذ الروتين الفرعي، تستمر الدالة
loginفي التنفيذ ويتم عرض النتيجة، ربما قبل انتهاء طلب الشبكة. يُرجى العِلم أنّه لتسهيل الأمر، سيتم تجاهل استجابة الشبكة في الوقت الحالي.
بما أنّ هذه الروتين الفرعي يبدأ باستخدام viewModelScope، يتم تنفيذه في نطاق ViewModel. إذا تم إتلاف ViewModel لأنّ المستخدم ينتقل إلى شاشة أخرى، سيتم إلغاء viewModelScope تلقائيًا، وسيتم أيضًا إلغاء جميع الروتينات الفرعية التي يتم تنفيذها.
تتمثّل إحدى المشاكل في المثال السابق في أنّ أي عملية استدعاء للدالة
makeLoginRequest يجب أن تتضمّن نقل التنفيذ بشكل صريح إلى خارج
الخيط الرئيسي. لنرى كيف يمكننا تعديل Repository لحل هذه المشكلة.
استخدام الروتينات المشتركة لضمان أمان سلسلة التعليمات الرئيسية
نعتبر الدالة آمنة للسلسلة الرئيسية عندما لا تحظر تعديلات واجهة المستخدم على السلسلة الرئيسية. الدالة makeLoginRequest ليست آمنة للاستخدام في السلسلة الرئيسية، لأنّ استدعاء makeLoginRequest من السلسلة الرئيسية يؤدي إلى حظر واجهة المستخدم. استخدِم الدالة withContext() من مكتبة الكوروتينات لنقل تنفيذ كوروتين إلى سلسلة محادثات مختلفة:
class LoginRepository( // ... ) { // ... suspend fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { // Move the execution of the coroutine to the I/O dispatcher return withContext(Dispatchers.IO) { // Blocking network request code } } }
تنقل withContext(Dispatchers.IO) عملية تنفيذ الروتين المشترك إلى سلسلة تعليمات خاصة بعمليات الإدخال والإخراج، ما يجعل الدالة التي تستدعيها آمنة للاستخدام في السلسلة الرئيسية، ويتيح تحديث واجهة المستخدم حسب الحاجة.
يتم أيضًا وضع علامة suspend على makeLoginRequest. هذه الكلمة الرئيسية هي طريقة Kotlin لفرض استدعاء دالة من داخل روتين فرعي.
في المثال التالي، يتم إنشاء الروتين الفرعي في LoginViewModel.
بما أنّ makeLoginRequest تنقل التنفيذ خارج سلسلة التعليمات الرئيسية، يمكن الآن تنفيذ الروتين الفرعي في الدالة login في سلسلة التعليمات الرئيسية:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine on the UI thread viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" // Make the network call and suspend execution until it finishes val result = loginRepository.makeLoginRequest(jsonBody) // Display result of the network request to the user when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
يُرجى العِلم أنّه لا يزال من الضروري استخدام الروتين الفرعي هنا، لأنّ makeLoginRequest هي دالة suspend، ويجب تنفيذ جميع دوال suspend في روتين فرعي.
يختلف هذا الرمز عن مثال login السابق في عدة جوانب:
- لا تقبل
launchالمَعلمةDispatchers.IO. عندما لا تمرِّرDispatcherإلىlaunch، سيتم تشغيل أي إجراءات روتينية مشتركة تم إطلاقها منviewModelScopeفي سلسلة التعليمات الرئيسية. - تتم الآن معالجة نتيجة طلب الشبكة لعرض واجهة المستخدم التي تشير إلى النجاح أو الفشل.
يتم الآن تنفيذ دالة تسجيل الدخول على النحو التالي:
- يستدعي التطبيق الدالة
login()من الطبقةViewفي سلسلة التعليمات الرئيسية. - تنشئ
launchكوروتينًا جديدًا في سلسلة التعليمات الرئيسية، ويبدأ الكوروتين في التنفيذ. - ضمن الروتين الفرعي، يؤدي استدعاء
loginRepository.makeLoginRequest()now إلى تعليق التنفيذ الإضافي للروتين الفرعي إلى أن ينتهي تشغيل كتلةwithContextفيmakeLoginRequest(). - بعد انتهاء تنفيذ الحظر في
withContext، تستأنف الروتين الفرعي فيlogin()على سلسلة التعليمات البرمجية الرئيسية مع نتيجة طلب الشبكة.
التعامل مع الاستثناءات
للتعامل مع الاستثناءات التي يمكن أن تطرحها طبقة Repository، استخدِم الدعم المضمّن للاستثناءات في Kotlin.
في المثال التالي، نستخدم كتلة try-catch:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" val result = try { loginRepository.makeLoginRequest(jsonBody) } catch (e: Exception) { Result.Error(Exception("Network request failed")) } when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
في هذا المثال، يتم التعامل مع أي استثناء غير متوقّع يتم طرحه بواسطة makeLoginRequest()
على أنّه خطأ في واجهة المستخدم.
مراجع إضافية حول الروتينات المشتركة
لإلقاء نظرة أكثر تفصيلاً على الكوروتينات في Android، يمكنك الاطّلاع على تحسين أداء التطبيق باستخدام الكوروتينات في Kotlin.
لمزيد من المراجع حول الروتينات المشتركة، يُرجى الاطّلاع على الروابط التالية: