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

الروتين المشترك هو نمط تصميم للتنفيذ المتزامن يمكنك استخدامه على 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 سلسلة UI الرئيسية عند إرسال طلب الشبكة. أبسط حلّ لنقل التنفيذ من سلسلة التعليمات الرئيسية هو إنشاء روتين فرعي جديد وتنفيذ طلب الشبكة على سلسلة تعليمات خاصة بوحدات الإدخال والإخراج:

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 محدّد مسبقًا ومضمّن في إضافات ViewModel KTX. يُرجى العِلم أنّه يجب تشغيل جميع الروتينات الفرعية في نطاق. يدير 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) عملية تنفيذ الروتين المشترك إلى سلسلة I/O، ما يجعل الدالة التي تستدعيها آمنة على السلسلة الرئيسية ويتيح لواجهة المستخدم إجراء التحديثات حسب الحاجة.

يتم أيضًا وضع علامة 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.

لمزيد من المراجع حول الروتينات المشتركة، يُرجى الاطّلاع على الروابط التالية: