الكوروتين عبارة عن نمط لتصميم التزامن يمكنك استخدامه على 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
مؤشر ترابط واجهة المستخدم عند تقديم طلب الشبكة. إن أبسط حل لنقل التنفيذ خارج سلسلة المحادثات الرئيسية هو إنشاء كوروتين جديد وتنفيذ طلب الشبكة على سلسلة محادثات I/O:
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
يشير إلى أنه يجب تنفيذ الكوروتين في سلسلة محادثات محجوزة لعمليات I/O.
يتم تنفيذ الدالة login
على النحو التالي:
- يستدعي التطبيق الدالة
login
من طبقةView
في سلسلة المحادثات الرئيسية. - ينشئ
launch
كوروتين جديدًا، ويتم إجراء طلب الشبكة بشكل مستقل على سلسلة محادثات محجوزة لعمليات I/O. - أثناء تشغيل الكورتين، تستمر الدالة
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)
على نقل تنفيذ الكورتين إلى سلسلة محادثات I/O، مما يجعل وظيفة الاتصال آمنة تمامًا ويتم تمكين واجهة المستخدم بالتحديث حسب الحاجة.
كما تم تمييز 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 makeLoginRequest(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.
لمزيد من موارد الكوروتينات، راجع الروابط التالية: