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

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