الكوروتين هو نمط تصميم متزامن يمكنك استخدامه على Android لتبسيط الرمز البرمجي الذي يتم تنفيذه بشكل غير متزامن. تمت إضافة الكوروتينات إلى لغة البرمجة Kotlin في الإصدار 1.3 وتستند إلى مفاهيم مُستخدَمة في لغات أخرى.
على نظام التشغيل Android، تساعد الكوروتينات في إدارة المهام طويلة الأمد التي قد تحظر سلسلة التعليمات الرئيسية بخلاف ذلك وتتسبب في عدم استجابة التطبيق. أفاد أكثر من 50٪ من المطورين المحترفين الذين يستخدمون الكوروتينات شهدوا زيادة في الإنتاجية. يصف هذا الموضوع كيف يمكنك استخدام الكوروتينات في لغة Kotlin لمعالجة هذه المشاكل، ما يتيح لك كتابة رمز تطبيق أنظف وأكثر إيجازًا.
الميزات
تُعدّ الكوروتينات الحل الذي ننصح به للبرمجة غير المتزامنة على Android. تشمل الميزات البارزة ما يلي:
- خفيف الوزن: يمكنك تشغيل العديد من الكوروتينات على سلسلة تعليمات واحدة بسبب دعم عملية التعليق التي لا تؤدي إلى حظر سلسلة التعليمات التي يتم فيها تشغيل الكوروتين. يؤدي التعليق إلى توفير الذاكرة أكثر من الحظر مع دعم العديد من العمليات المتزامنة.
- تقليل حالات تسرّب الذاكرة: استخدِم تزامنًا منظَّمًا لتشغيل العمليات ضِمن نطاق.
- إتاحة الإلغاء المضمَّنة: يتم نشر الإلغاء تلقائيًا من خلال التسلسل الهرمي الحالي لكروتين.
- التكامل مع Jetpack: تشتمل العديد من مكتبات Jetpack على إضافات توفّر توافقًا كاملاً مع الكوروتينات. توفّر بعض المكتبات أيضًا نطاق الكوروتين الخاص بها الذي يمكنك استخدامه لإنشاء تزامن منظم.
نظرة عامة على الأمثلة
استنادًا إلى دليل بنية التطبيق، تقدّم الأمثلة في هذا الموضوع طلب الشبكة وتعرض النتيجة إلى سلسلة التعليمات الرئيسية حيث يمكن للتطبيق عرض النتيجة للمستخدم.
على وجه التحديد، يستدعي مكوِّن البنية في ViewModel
طبقة المستودع على سلسلة التعليمات الرئيسية
لتفعيل طلب الشبكة. يتكرّر هذا الدليل من خلال الحلول المختلفة التي تستخدم
الكوروتينات التي تحافظ على حظر السلسلة الرئيسية.
يتضمّن ViewModel
مجموعة من إضافات KTX التي تعمل مباشرةً مع الكوروتينات. هذه الإضافات هي
مكتبة lifecycle-viewmodel-ktx
ويتم استخدامها
في هذا الدليل.
المعلومات المتعلقة بالاعتمادية
لاستخدام الكوروتين في مشروع Android، أضِف التبعية التالية
إلى ملف build.gradle
في تطبيقك:
رائع
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>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// 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)
}
}
}
لنحلّل رمز الكوروتينات في الدالة login
:
viewModelScope
هو معرّفCoroutineScope
محدَّد مسبقًا ومضمّن في إضافاتViewModel
KTX. لاحظ أن جميع الكوروتينات يجب أن تعمل في نطاق. يديرCoroutineScope
واحدًا أو أكثر من عناصر الكوروتين ذات الصلة.launch
هي دالة تنشئ الكوروتين وترسل تنفيذ نص الدالة إلى المرسل المقابل.- تشير السمة
Dispatchers.IO
إلى أنّه يجب تنفيذ هذا الكوروتين على سلسلة ترابط محجوزة لعمليات الإدخال والإخراج.
يتم تنفيذ الدالة login
على النحو التالي:
- يستدعي التطبيق الدالة
login
من طبقةView
في سلسلة التعليمات الرئيسية. - تنشئ
launch
الكوروتين الجديد، ويتم تقديم طلب الشبكة بشكل مستقل على سلسلة تعليمات محجوزة لعمليات وحدات الإدخال والإخراج. - أثناء تشغيل الكوروتين، تستمر الدالة
login
في التنفيذ والرجوع، ربما قبل الانتهاء من طلب الشبكة. لاحظ أنه لغرض البساطة، يتم تجاهل استجابة الشبكة في الوقت الحالي.
بما أنّ هذا الكوروتين يبدأ بـ viewModelScope
، يتم تنفيذه في نطاق ViewModel
. في حال تلف ViewModel
بسبب ابتعاد المستخدم عن الشاشة، يتم إلغاء viewModelScope
تلقائيًا، كما يتم إلغاء جميع الكوروتينات قيد التشغيل أيضًا.
إحدى المشاكل في المثال السابق هي أنّ أي طلب اتصال
بـ makeLoginRequest
يحتاج إلى تذكّر نقل عملية التنفيذ بشكل صريح إلى سلسلة التعليمات الرئيسية. لنرَ كيف يمكننا تعديل Repository
لحل هذه المشكلة بالنسبة لنا.
استخدام الكوروتين للحفاظ على الأمان الرئيسي
نعتبر دالة آمنة عندما لا تحظر تحديثات واجهة المستخدم في سلسلة التعليمات الرئيسية. إنّ دالة makeLoginRequest
غير آمنة بشكل رئيسي، لأنّ استدعاء
makeLoginRequest
من سلسلة التعليمات الرئيسية لا يؤدي إلى حظر واجهة المستخدم. استخدِم الدالة withContext()
من مكتبة coroutines لنقل تنفيذ الكوروتين إلى سلسلة تعليمات مختلفة:
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)
عملية تنفيذ الكوروتين إلى مؤشر ترابط وحدات الإدخال والإخراج، ما يجعل وظيفة الاتصال آمنة بشكل رئيسي مع إتاحة تحديث واجهة المستخدم حسب الحاجة.
تم أيضًا تمييز makeLoginRequest
باستخدام الكلمة الرئيسية suspend
. هذه الكلمة الرئيسية هي طريقة لغة 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()
في الوقت الحالي إلى تعليق تنفيذ المزيد من الكوروتين إلى أن ينتهي تشغيل الجزء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.
للحصول على مزيد من موارد الكوروتين، اطلع على الروابط التالية:
- نظرة عامة على الكوروتين (JetBrains)
- دليل الكوروتين (JetBrains)
- مراجع إضافية لتدفقات الكوروتينات في لغة Kotlin