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

الكوروتين هو نمط تصميم متزامن يمكنك استخدامه على تبسيط الرمز الذي يتم تنفيذه بشكل غير متزامن في Android الكوروتين إلى Kotlin في الإصدار 1.3 وتستند إلى المفاهيم من اللغات الأخرى.

على نظام Android، يساعد الكوروتين في إدارة المهام الطويلة المدى التي قد حظر سلسلة التعليمات الرئيسية وجعل التطبيق لا يستجيب. أفاد أكثر من 50% من المطوّرين المحترفين الذين يستخدمون الكوروتينات بأنهم زيادة الإنتاجية. يصف هذا الموضوع كيفية استخدام الكوروتينات في لغة Kotlin لمعالجة هذه مما يتيح لك كتابة كود تطبيق أكثر وضوحًا وإيجازًا.

الميزات

الكوروتينات هو الحل الذي ننصح به للبرمجة غير المتزامنة على Android وتشمل الميزات البارزة ما يلي:

  • خفيفة الوزن: يمكنك تشغيل العديد من الكوروتينات في سلسلة محادثات واحدة بسبب الدعم لـ التعليق، وهذا لا يحظر سلسلة المحادثات التي يعمل بها الكوروتين. جارٍ التعليق ويحفظ الذاكرة خلال الحظر مع دعم العديد من العمليات المتزامنة.
  • عدد أقل من تسريب الذاكرة: استخدم التزامن المنظّم لتشغيل العمليات ضمن النطاق.
  • إمكانية إلغاء الاشتراك المضمَّنة: الإلغاء تلقائيًا من خلال تسلسل الكوروتينات الجاري.
  • دمج Jetpack: تتضمن العديد من مكتبات Jetpack. الإضافات التي توفّر دعم الكوروتينات بالكامل بعض الإشعارات المكتبات أيضًا توفر نطاق الكوروتين الذي يمكنك للاستخدام في التزامن المنظم.

نظرة عامة على الأمثلة

استنادًا إلى دليل بنية التطبيق، يمكن العثور على في هذا الموضوع، أرسل طلبًا عبر الشبكة وارجع النتيجة إلى حيث يمكن للتطبيق عرض النتيجة للمستخدم.

وعلى وجه التحديد، ViewModel يستدعي مكوِّن البنية طبقة المستودع في سلسلة التعليمات الرئيسية تشغيل طلب الشبكة. يتكرر هذا الدليل عبر عدة حلول التي تستخدم الكوروتينات لإبقاء سلسلة التعليمات الرئيسية غير محظورة.

تضم 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>()
}

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 محدَّد مسبقًا يتم تضمينه مع إضافات KTX ViewModel. لاحظ أنه يجب تشغيل جميع الكوروتينات في النطاق. يدير CoroutineScope واحد أو أكثر من الكوروتينات ذات الصلة.
  • launch هي دالة تنشئ الكوروتين وإرسال تنفيذ نص وظيفته إلى المرسل المقابل.
  • تشير السمة Dispatchers.IO إلى أنّه يجب تنفيذ هذا الكوروتين على تم حجز سلسلة التعليمات لعمليات الإدخال والإخراج.

يتم تنفيذ الدالة login على النحو التالي:

  • يستدعي التطبيق الدالة login من طبقة View في سلسلة المحادثات الرئيسية.
  • ينشئ launch كوروتينًا جديدًا، ويتم تنفيذ طلب الشبكة. بشكل مستقل على سلسلة محادثات محجوزة لعمليات الإدخال والإخراج.
  • أثناء تشغيل الكوروتين، تستمر وظيفة login في التنفيذ. وعمليات الإرجاع، ربما قبل الانتهاء من طلب الشبكة. لاحظ أن للتبسيط، يتم تجاهل استجابة الشبكة في الوقت الحالي.

بما أنّ هذا الكوروتين بدأ باستخدام viewModelScope، يتم تنفيذه في نطاق ViewModel. إذا تم تلف ViewModel بسبب انتقال المستخدم بعيدًا عن الشاشة، يتم تلقائيًا نقل "viewModelScope" وإلغاء جميع الكوروتينات الجارية أيضًا.

إحدى مشكلات المثال السابق هي أن أي شيء يتصل يحتاج makeLoginRequest إلى تذكُّر نقل عملية التنفيذ بشكل صريح سلسلة التعليمات الرئيسية. لنرَ كيف يمكننا تعديل Repository لحلّ هذه المشكلة لنا.

استخدام الكوروتين للحفاظ على السلامة الرئيسية

نعتبر الدالة main-safe عندما لا تحظر تحديثات واجهة المستخدم على السلسلة الرئيسية. الدالة 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) ينقل تنفيذ الكوروتين إلى تعمل سلسلة وحدات الإدخال والإخراج على جعل وظيفة الاستدعاء آمنة بشكل رئيسي وتمكين واجهة المستخدم تحديثه حسب الحاجة.

يتميز 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 طرحها، استخدِم واجهة برمجة تطبيقات دعم مضمَّن للاستثناءات. في المثال التالي، نستخدم كتلة 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

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