الكوروتينات في لغة Kotlin على Android

الكوروتين هو نمط تصميم متزامن يمكنك استخدامه على 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.

للحصول على مزيد من موارد الكوروتين، اطلع على الروابط التالية: