في هذا الدرس التطبيقي، ستتعرّف على كيفية استخدام Kotlin Coroutines في تطبيق متوافق مع Android، وهي طريقة جديدة لإدارة سلاسل المحادثات في الخلفية التي يمكنها تبسيط الرمز عن طريق تقليل الحاجة إلى معاودة الاتصال. الكوروتينات هي ميزة بلغة Kotlin التي تعمل على تحويل استدعاءات غير متزامنة للمهام التي تستغرق مدة طويلة، مثل الوصول إلى قاعدة البيانات أو الشبكة، إلى رمز تسلسلي.
في ما يلي مقتطف رمز لمنحك فكرة عمّا ستفعله.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
سيتم تحويل الرمز المستند إلى معاودة الاتصال إلى رمز تسلسلي باستخدام الكوروتينات.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
ستبدأ باستخدام تطبيق حالي تم إنشاؤه باستخدام مكونات الهندسة المعمارية الذي يستخدم نمط معاودة اتصال للمهام التي تستغرق وقتًا طويلاً.
بنهاية هذا الدرس التطبيقي، ستحظى بخبرة كافية لاستخدام الكوروتينات في تطبيقك لتحميل البيانات من الشبكة، وستتمكن من دمج الكوروتينات في أحد التطبيقات. وستكون على دراية أيضًا بأفضل الممارسات لاستخدام الكوروتينات، وكيفية كتابة اختبار على الرمز الذي يستخدم الكوروتينات.
المتطلبات الأساسية
- التعرّف على مكوّنات الهندسة
ViewModel
وLiveData
وRepository
وRoom
. - خبرة في بنية Kotlin، بما في ذلك وظائف الإضافات وlambda.
- فهم أساسي لاستخدام سلاسل المحادثات على نظام التشغيل Android، بما في ذلك سلسلة المحادثات الرئيسية وسلاسل المحادثات في الخلفية وعمليات معاودة الاتصال.
المهام التي ستنفِّذها
- رمز الاتصال المكتوب باستخدام الكوروتينات والحصول على النتائج
- يمكنك استخدام دوال التعليق لجعل الرمز غير متسلسل.
- استخدِم
launch
وrunBlocking
للتحكّم في كيفية تنفيذ الرمز. - تعرّف على أساليب تحويل واجهات برمجة التطبيقات الحالية إلى الكوروتينات باستخدام
suspendCoroutine
. - استخدام الكوروتين مع المكونات المعمارية
- تعرَّف على أفضل الممارسات لاختبار الكوروتينات.
الأشياء التي تحتاج إليها
- Android Studio 3.5 (قد يعمل الدرس التطبيقي مع الإصدارات الأخرى، ولكن قد تكون بعض الميزات غير متوفّرة أو مختلفة).
إذا واجهت أي مشاكل (أخطاء الترميز، أو الأخطاء النحوية، أو الصياغة غير الواضحة، وما إلى ذلك) أثناء معالجة هذا الدرس التطبيقي حول الترميز، يُرجى الإبلاغ عن المشكلة من خلال الرابط الإبلاغ عن خطأ في أسفل يمين الدرس التطبيقي حول الترميز.
تنزيل الرمز
انقر على الرابط التالي لتنزيل كل رموز هذا الدرس التطبيقي حول الترميز:
... أو استنساخ مستودع GitHub من سطر الأوامر باستخدام الأمر التالي:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
الأسئلة الشائعة
أولاً، لنرى كيف يبدو نموذج بدء التشغيل. يُرجى اتّباع التعليمات التالية لفتح نموذج التطبيق في "استوديو Android".
- في حال تنزيل ملف ZIP بتنسيق
kotlin-coroutines
، يُرجى فك ضغط الملف. - افتح مشروع
coroutines-codelab
في "استوديو Android". - اختَر وحدة التطبيق
start
. - انقر على الزر
تشغيل، واختَر محاكيًا أو اربط جهازك الذي يعمل بنظام التشغيل Android، والذي يجب أن يكون قادرًا على تشغيل Android Lollipop (الحد الأدنى المتوافق مع حزمة تطوير البرامج (SDK) هو 21). ويجب أن تظهر شاشة Kotlin Coroutines:
يستخدم تطبيق إجراء التفعيل هذا سلاسل المحادثات لزيادة العدد لفترة قصيرة بعد الضغط على الشاشة. سيؤدي ذلك أيضًا إلى جلب عنوان جديد من الشبكة وعرضه على الشاشة. جرِّب الميزة الآن، ومن المفترض أن تلاحظ تغييرًا في العدد والرسالة بعد مهلة قصيرة. في هذا الدرس التطبيقي حول الترميز، عليك تحويل هذا التطبيق لاستخدام الكوروتين.
يستخدم هذا التطبيق "مكونات البنية" لفصل رمز واجهة المستخدم في MainActivity
عن منطق التطبيق في MainViewModel
. يُرجى تخصيص بعض الوقت للتعرّف على بنية المشروع.
- تعرض
MainActivity
واجهة المستخدم، كما تُسجِّل مستمعي النقرات، ويمكنها عرضSnackbar
. سيؤدي ذلك إلى تمرير الأحداث إلىMainViewModel
وتعديل الشاشة استنادًا إلىLiveData
فيMainViewModel
. - يتعامل
MainViewModel
مع الأحداث فيonMainViewClicked
وسيتم التواصل معMainActivity
باستخدامLiveData.
- تحدّد السمة
Executors
السمةBACKGROUND,
التي يمكنها تشغيل العناصر في سلسلة محادثات في الخلفية. - يجلب
TitleRepository
النتائج من الشبكة ويحفظها في قاعدة البيانات.
إضافة الكوروتينات إلى مشروع
لاستخدام الكوروتينات في لغة Kotlin، يجب تضمين مكتبة coroutines-core
في ملف build.gradle (Module: app)
لمشروعك. لقد نجحت مشاريع الدروس التطبيقية في تنفيذ ذلك نيابةً عنك، لذا لست بحاجة إلى إجراء ذلك لإكمال الدرس التطبيقي حول الترميز.
تتوفّر الكوروتينات على نظام التشغيل Android كمكتبة أساسية وإضافات مخصّصة لنظام التشغيل Android:
- kotlinx-corountines-core : الواجهة الرئيسية لاستخدام الكوروتينات في لغة Kotlin
- kotlinx-coroutines-android — دعم سلسلة Android الرئيسية في الكوروتين
يتضمّن تطبيق "بدء التشغيل" تبعيات في build.gradle.
عند إنشاء مشروع تطبيق جديد، عليك فتح build.gradle (Module: app)
وإضافة تبعيات الكوروتين إلى المشروع.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
بالنسبة إلى Android، من الضروري تجنب حظر السلسلة الرئيسية. سلسلة المحادثات الرئيسية هي سلسلة محادثات واحدة تعالج جميع التحديثات في واجهة المستخدم. كما أنها تتضمن أيضًا سلسلة المحادثات التي تطلب جميع معالجات النقر واستدعاءات واجهة المستخدم الأخرى. وبذلك، يجب أن يتم تشغيله بسلاسة لضمان تجربة مستخدم رائعة.
لكي يتم عرض تطبيقك للمستخدم بدون أي عمليات إيقاف مرئية مرئية، يجب أن تحدِّد سلسلة المحادثات الرئيسية الشاشة كل 16 ملّي ثانية أو أكثر، أي حوالي 60 لقطة في الثانية. تستغرق العديد من المهام الشائعة وقتًا أطول من ذلك، مثل تحليل مجموعات بيانات JSON كبيرة أو كتابة البيانات في قاعدة بيانات أو جلب البيانات من الشبكة. ولذلك، قد يؤدي طلب رمز بهذه الطريقة من سلسلة المحادثات الرئيسية إلى إيقاف التطبيق مؤقتًا أو تقطعه أو حتى توقفه عن العمل. وإذا حظرت سلسلة المحادثات الرئيسية لفترة طويلة جدًا، قد يتعطّل التطبيق ويعرض مربّع حوار التطبيق لا يستجيب.
شاهد الفيديو أدناه للحصول على مقدمة حول كيفية حل الكوروتينات لهذه المشكلة على Android عن طريق تقديم الأمان الرئيسي.
نمط معاودة الاتصال
هناك نمط واحد لتنفيذ المهام التي تستغرق مدة طويلة بدون حظر سلسلة المحادثات الرئيسية، وهو استدعاءات الاستدعاء. باستخدام استدعاءات، يمكنك بدء المهام التي تستغرق مدة طويلة في سلسلة محادثات في الخلفية. عند اكتمال المهمة، يتم استدعاء معاودة الاتصال لإبلاغك بالنتيجة في سلسلة المحادثات الرئيسية.
ألقِ نظرة على مثال على نمط معاودة الاتصال.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
ولأنّ هذا الرمز يتضمّن تعليقًا توضيحيًا بعنوان @UiThread
، يجب أن يتم تشغيله بسرعة كافية لتنفيذ سلسلة المحادثات الرئيسية. وهذا يعني أنّه يجب الرجوع بسرعة كبيرة، حتى لا يتأخر تعديل الشاشة التالي. ولكن بما أن اكتمال slowFetch
يستغرق ثوانٍ أو حتى دقائق، لا يمكن لسلسلة المحادثات الرئيسية الانتظار للحصول على النتيجة. يسمح استدعاء show(result)
بتشغيل slowFetch
في سلسلة محادثات في الخلفية وعرض النتيجة عندما تكون جاهزة.
استخدام الكوروتينات لإزالة عمليات رد الاتصال
تتميز عمليات معاودة الاتصال بنمط رائع، ولكن هناك بعض السلبيات. قد يصبح من الصعب قراءة الرمز الذي يستخدم معاودة الاتصال بشدّة وتزداد صعوبة التفكير في أسبابه. بالإضافة إلى ذلك، لا تسمح معاودة الاتصال باستخدام بعض ميزات اللغة، مثل الاستثناءات.
تسمح لك الكوروتينات بلغة Kotlin بتحويل الرمز المستند إلى معاودة الاتصال إلى رمز تسلسلي. عادةً ما تسهل قراءة الرمز المكتوب بالتسلسل، ويمكن استخدام ميزات اللغة، مثل الاستثناءات.
في النهاية، ينفذ الفريق الإجراء نفسه: الانتظار حتى تصبح النتيجة متاحة من مهمة طويلة الأمد ومتابعة التنفيذ. ومع ذلك، تبدو الرموز مختلفة تمامًا.
الكلمة الرئيسية suspend
هي طريقة Kotlin' لوضع علامة على دالة أو نوع دالة متاح للكوروتينات. عندما يستدعي كورتين دالة موضوع عليها علامة suspend
، بدلاً من الحظر حتى تعود هذه الدالة كاستدعاء دالة عادي، تعلق التنفيذ حتى تصبح النتيجة جاهزة، ثم يتم استئنافها من حيث توقفت مع النتيجة. أثناء تعليقه في انتظار النتيجة، يتم إلغاء حظر سلسلة المحادثات التي يتم تشغيلها، بحيث يمكن تشغيل الوظائف أو الكوروتينات الأخرى.
على سبيل المثال، في الرمز أدناه، makeNetworkRequest()
وslowFetch()
هما أدالتان suspend
.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
وكما هو الحال مع إصدار معاودة الاتصال، يجب أن يعود makeNetworkRequest
من سلسلة المحادثات الرئيسية على الفور لأنه تم وضع علامة عليه للإشارة إلى @UiThread
. وهذا يعني أنّه لا يمكن عادةً الاتصال بأساليب حظر مثل slowFetch
. في ما يلي الكلمات الرئيسية التي تؤدي فيها الكلمة الرئيسية suspend
سحرها.
مقارنةً بالرمز المستند إلى معاودة الاتصال، يحقق رمز كوروتين النتيجة نفسها لإزالة حظر سلسلة المحادثات الحالية باستخدام رمز أقل. نظرًا لنمطها التسلسلي، فإنه من السهل ربط عدة مهام تستغرق وقتًا طويلاً بدون إنشاء استدعاءات متعددة. على سبيل المثال، يمكن كتابة الرمز الذي يجلب نتيجة من نقطتَي نهاية للشبكة وحفظه في قاعدة البيانات كدالة في الكوروتين بدون استدعاءات. على النحو التالي:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
يمكنك إدخال الكوروتينات في نموذج التطبيق في القسم التالي.
في هذا التمرين، ستكتب "كوروتين" لعرض رسالة بعد فترة تأخير. للبدء، احرص على فتح الوحدة start
في"استوديو Android".
فهم نطاق الكوروتين
في لغة Kotlin، تعمل كل الكوروتين في CoroutineScope
. يتحكم النطاق في فترة بقاء الكوروتينات من خلال وظيفتها. عند إلغاء مهمة نطاق، يؤدي ذلك إلى إلغاء جميع الكوروتينات التي بدأت في ذلك النطاق. على نظام التشغيل Android، يمكنك استخدام نطاق لإلغاء كل الكوروتينات قيد التشغيل، مثلاً، عندما ينتقل المستخدم بعيدًا عن Activity
أو Fragment
. تتيح لك النطاقات أيضًا تحديد المرسل التلقائي. يتحكم المُرسل في سلسلة المحادثات التي تشغّل كوروتين.
بالنسبة إلى الكوروتينات التي بدأتها واجهة المستخدم، من الصحيح أن تبدأ في Dispatchers.Main
وهو سلسلة المحادثات الرئيسية على نظام التشغيل Android. لن يؤدي كورنوتين في Dispatchers.Main
إلى حظر سلسلة المحادثات الرئيسية أثناء تعليقه. بما أنّ كورنوتين ViewModel
غالبًا ما يحدّث واجهة المستخدم في سلسلة المحادثات الرئيسية، يمكن أن يؤدي بدء الكوروتينات في سلسلة المحادثات الرئيسية إلى حفظ مفاتيح التبديل لسلاسل المحادثات الإضافية. يمكن أن يؤدي تشغيل كورتين في السلسلة الرئيسية إلى تبديل المُرسلات في أي وقت بعد بدء تشغيله. على سبيل المثال، يمكنها استخدام مُرسل آخر لتحليل نتيجة JSON كبيرة خارج سلسلة المحادثات الرئيسية.
استخدام viewmodelScope
تضيف مكتبة AndroidX lifecycle-viewmodel-ktx
نطاق CoroutineScope إلى Viewنماذج
الذي يتم ضبطه لبدء الكوروتينات المتعلقة بواجهة المستخدم. لاستخدام هذه المكتبة، يجب تضمينها في ملف build.gradle (Module: start)
لمشروعك. سبق أن تم تنفيذ هذه الخطوة في مشاريع الدروس التطبيقية.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
تضيف المكتبة دالة viewModelScope
كوظيفة إضافة للصف ViewModel
. هذا النطاق مرتبط بـ Dispatchers.Main
وسيتم إلغاؤه تلقائيًا عند محو ViewModel
.
التبديل من سلاسل المحادثات إلى الكوروتين
في MainViewModel.kt
، ابحث عن المهام التالية مع هذا الرمز:
MainViewmodel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
يستخدم هذا الرمز BACKGROUND ExecutorService
(المحدّد في util/Executor.kt
) لتشغيله في سلسلة محادثات في الخلفية. ونظرًا لأن sleep
يحظر سلسلة المحادثات الحالية، سيؤدي ذلك إلى تجميد واجهة المستخدم إذا تم استدعاؤها في سلسلة المحادثات الرئيسية. بعد ثانية واحدة من نقر المستخدم على العرض الرئيسي، يطلب شريطًا للوجبات الخفيفة.
يمكن رؤية حدوث هذا من خلال إزالة BACKGROUND من الرمز وتشغيله مرة أخرى. لن يتم عرض مؤشر سريان العمل ولن يتم عرض كل شيء على الفور بعد الانتقال إلى الحالة النهائية.
MainViewmodel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
استبدِل updateTaps
بهذا الرمز المرتكز على الكوروتين الذي ينفّذ الإجراء نفسه. سيكون عليك استيراد launch
وdelay
.
MainViewmodel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
ويتّخذ هذا الرمز الإجراء نفسه، مع الانتظار ثانية واحدة قبل عرض شريط للوجبات الخفيفة. ومع ذلك، هناك بعض الاختلافات المهمة:
viewModelScope.
launch
سيبدأ كورتين فيviewModelScope
. وهذا يعني أنه عند إلغاء الوظيفة التي أرسلناها إلىviewModelScope
، سيتم إلغاء كل الكوروتينات في هذه الوظيفة/النطاق. إذا غادر المستخدم النشاط قبل إرجاعdelay
، سيتم إلغاء هذا الكوروتين تلقائيًا عند استدعاءonCleared
عند تدمير المشاهدة.- وبما أن
viewModelScope
هو المُرسِل التلقائي لـDispatchers.Main
، سيتم تشغيل هذا الكوروتين في سلسلة المحادثات الرئيسية. سنتعرّف لاحقًا على كيفية استخدام سلاسل محادثات مختلفة. - الدالة
delay
هي دالةsuspend
. يظهر هذا الرمز في "استوديو Android" من خلال رمزفي بالوعة اليمنى. على الرغم من أن هذا المضايقة يعمل على سلسلة المحادثات الرئيسية، لن يحظر
delay
سلسلة المحادثات لمدة ثانية واحدة. وبدلاً من ذلك، سيعمل المُرسل على جدولة الكوروتين لاستئنافه في ثانية واحدة في العبارة التالية.
أَرْجُو الْمُتَابْعَة وِتَشْغِيلِ الْإِذْنْ. عند النقر على طريقة العرض الرئيسية، سيظهر لك شريط للوجبات الخفيفة بعد ثانية واحدة.
في القسم التالي، سنتناول كيفية اختبار هذه الدالة.
في هذا التمرين، ستكتب اختبارًا للرمز الذي كتبته للتو. يعرض لك هذا التمرين طريقة اختبار الكوروتينات في Dispatchers.Main
باستخدام مكتبة kotlinx-coroutines-test. في وقت لاحق من هذا الدرس التطبيقي، ستنفِّذ اختبارًا يتفاعل مع الكوروتينات مباشرةً.
مراجعة الرمز الحالي
افتح MainViewModelTest.kt
في مجلد androidTest
.
MainViewmodelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
القاعدة هي طريقة لتشغيل الرمز قبل تنفيذ اختبار في الوحدة التنظيمية وبعده. يتم استخدام قاعدتين للسماح لنا باختبار MainView تكرار في اختبار خارج الجهاز:
InstantTaskExecutorRule
هي قاعدة JUnit التي تضبطLiveData
لتنفيذ كل مهمة بشكل متزامن.MainCoroutineScopeRule
هي قاعدة مخصّصة في قاعدة الرموز هذه تعمل على إعدادDispatchers.Main
لاستخدامTestCoroutineDispatcher
منkotlinx-coroutines-test
. ويسمح ذلك للاختبارات بتقدّم الساعة الافتراضية للاختبار، كما يسمح للرمز باستخدامDispatchers.Main
في اختبارات الوحدات.
باستخدام الطريقة setup
، يتم إنشاء مثيل جديد من MainViewModel
باستخدام اختبار مزيف - هذه عمليات تنفيذ زائفة للشبكة وقاعدة البيانات المقدمة في رمز بدء التشغيل للمساعدة في كتابة الاختبارات بدون استخدام الشبكة أو قاعدة البيانات الحقيقية.
في هذا الاختبار، لا يلزم استخدام أي سلع مزيّفة إلا لتلبية تبعيات MainViewModel
. في وقت لاحق من هذا الدرس التطبيقي حول الترميز، ستعدّل العناصر المزيفة لدعم الكوروتين.
كتابة اختبار يتحكّم في الكوروتين
إضافة اختبار جديد يضمن تحديث النقرات بعد ثانية واحدة من النقر على العرض الأساسي:
MainViewmodelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
من خلال الاتصال بـ onMainViewClicked
، سيتم إطلاق الكوروتين الذي أنشأناه للتو. يتحقّق هذا الاختبار من أن نص النقرات لا يزال "0;&ttaps" بعد استدعاء onMainViewClicked
مباشرةً، ثم يتم تغييره بعد ثانية واحدة إلى "1;ttaps".
يستخدم هذا الاختبار الوقت الافتراضي للتحكم في تنفيذ الكوروتين الذي تم إطلاقه بواسطة onMainViewClicked
. يتيح MainCoroutineScopeRule
إيقاف الكوروتينات التي يتم إطلاقها على Dispatchers.Main
مؤقتًا أو استئنافها أو التحكّم فيها. هَذِهِ هِيَ اسْمُ الطَّلَبْ مِنْ advanceTimeBy(1_000)
الَّتِي سَتُؤَدِّي إِلَى تَنْفِيذِ الْمُرْسِلِ الرَّئِيسِي فَوْرًا.
هذا الاختبار نهائي تمامًا، ما يعني أنه سيتم تنفيذه بالطريقة نفسها دائمًا. ولأنها تتحكم بشكل كامل في تنفيذ الكوروتينات في Dispatchers.Main
، لا تحتاج إلى الانتظار لمدة ثانية واحدة حتى يتم ضبط القيمة.
إجراء الاختبار الحالي
- انقر بزر الماوس الأيمن على اسم الصف
MainViewModelTest
في المُحرِّر لفتح قائمة السياقات. - في قائمة السياقات، اختَر
Run 'MainViewmodelTest'
- بالنسبة إلى عمليات التشغيل المستقبلية، يمكنك اختيار الإعدادات التجريبية هذه في الإعدادات بجانب الزر
في شريط الأدوات. وسيُطلق على الإعدادات تلقائيًا اسم MainViewmodelTest.
من المفترض أن تظهر لك نتيجة الاختبار. ويُفترض ألا يستغرق التشغيل أقل من ثانية واحدة.
في التمرين التالي، ستتعرّف على كيفية التحويل من واجهات برمجة تطبيقات استدعاء حالية لاستخدام الكوروتين.
في هذه الخطوة، ستبدأ في تحويل مستودع إلى الكوروتينات. لإجراء ذلك، سنضيف الكوروتينات إلى ViewModel
وRepository
وRoom
وRetrofit
.
من المفيد التعرّف على الدور المسؤول عن كل جزء من البنية قبل التبديل إلى استخدام الكوروتينات.
- تنفّذ
MainDatabase
قاعدة بيانات باستخدام الغرفة التي تحفظTitle
وتحمّله. - تنفّذ
MainNetwork
واجهة برمجة تطبيقات للشبكة بغرض الحصول على عنوان جديد. وهو يستخدم Retrofit لجلب العناوين. تم ضبطRetrofit
على عرض أخطاء أو بيانات زائفة، إلا أنّه يعمل كما لو كان يقدّم طلبات شبكة فعلية. - تنفّذ
TitleRepository
واجهة برمجة تطبيقات واحدة لجلب العنوان أو إعادة تحميله من خلال الجمع بين البيانات من الشبكة وقاعدة البيانات. - تمثل الخاصية
MainViewModel
حالة الشاشة وتتعامل مع الأحداث. وسيطلب ذلك من المستودع إعادة تحميل العنوان عندما ينقر المستخدم على الشاشة.
وبما أنّ طلب الشبكة هذا يعتمد على أحداث واجهة المستخدم ونودّ بدء استخدام كوروتين استنادًا إليها، إنّ المكان الطبيعي لبدء استخدام الكوروتينات في ViewModel
.
إصدار معاودة الاتصال
افتح MainViewModel.kt
للاطّلاع على إعلان refreshTitle
.
MainViewmodel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
ويتم استدعاء هذه الوظيفة في كل مرة ينقر فيها المستخدم على الشاشة، ما يؤدي إلى إعادة تحميل المستودع للمكتبة وكتابة العنوان الجديد في قاعدة البيانات.
يستخدم هذا التنفيذ استدعاءً لتنفيذ بعض الإجراءات:
- قبل بدء طلب البحث، يعرض مؤشر سريان التحميل مع
_spinner.value = true
- عندما تحصل على نتيجة، تعمل على محو مؤشر التحميل الدوَّار باستخدام
_spinner.value = false
. - إذا ظهر خطأ، سيُطلب من شريط للوجبات الخفيفة عرض محوة مؤشر سريان العمل ومحوه.
يُرجى ملاحظة أن معاودة الاتصال على onCompleted
لم تتجاوز title
. وبما أننا نكتب جميع العناوين إلى قاعدة بيانات Room
، يتم تعديل واجهة المستخدم إلى العنوان الحالي من خلال ملاحظة LiveData
التي يتم تعديلها في Room
.
وفي التحديث الذي أجريناه على الكوروتينات، سنحافظ على السلوك نفسه. إنه نمط جيد لاستخدام مصدر بيانات قابل للملاحظة، مثل قاعدة بيانات Room
للحفاظ على تحديث واجهة المستخدم تلقائيًا.
إصدار الكوروتين
لنعيد كتابة refreshTitle
بالكوروتينات.
وبما أننا سنحتاج إليها على الفور، لنبدأ بإنشاء وظيفة تعليق فارغة في مستودعنا (TitleRespository.kt
). حدِّد دالة جديدة تستخدم عامل التشغيل suspend
لإبلاغ Kotlin بأنها تعمل مع الكوروتينات.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
وعند الانتهاء من هذا الدرس التطبيقي، عليك تعديل هذا الملف لاستخدام التحديث القديم والغرفة لجلب عنوان جديد وكتابته إلى قاعدة البيانات باستخدام الكوروتين. في الوقت الحالي، لن ينفق سوى 500 مللي ثانية ويتظاهر بأداء العمل ثم يستمر.
في MainViewModel
، يمكنك استبدال إصدار معاودة الاتصال من refreshTitle
بنسخة تشغّل كوروتين جديدًا:
MainViewmodel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
لنتعرّف على هذه الدالة:
viewModelScope.launch {
تمامًا مثل الكوروتين لتعديل عدد النقرات، ابدأ بإطلاق كوروتين جديد في viewModelScope
. سيؤدي هذا إلى استخدام القيمة Dispatchers.Main
والتي لا بأس بها. على الرغم من أن refreshTitle
سينشئ طلب شبكة وطلب بحث في قاعدة البيانات، فإنه يمكن أن يستخدم الكوروتينات لعرض واجهة الأساسية الآمنة. ويعني ذلك أنّه سيكون من الآمن الاتصال بهذا الموضوع من سلسلة المحادثات الرئيسية.
بما أننا نستخدم viewModelScope
، عندما ينتقل المستخدم بعيدًا عن هذه الشاشة، سيتم إلغاء العمل الذي بدأه هذا المحتوى التلقائي تلقائيًا. ويعني هذا أنّه لن يقدّم طلبات إضافية للشبكة أو طلبات بحث في قاعدة البيانات.
يطلب أسطر الرموز القليلة التالية refreshTitle
في repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
وقبل أن يبدأ هذا المملوك في تنفيذ أي شيء، يبدأ رمز سريان العمل في التحميل، ثم يتصل باسم refreshTitle
مثل الدالة العادية. ومع ذلك، بما أنّ refreshTitle
هي دالة تعليق، يتم تنفيذها بشكل مختلف عن الدالة العادية.
لست بحاجة إلى تمرير معاودة الاتصال. سيتم تعليق كوروتين إلى أن يتم استئنافه من قِبل refreshTitle
. على الرغم من أنّ الميزة تبدو تمامًا كاستدعاء وظيفة حظر عادية، ستنتظر تلقائيًا إلى أن تكتمل طلب البحث على الشبكة وقاعدة البيانات قبل استئناف سلسلة المحادثات الرئيسية بدون حظرها.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
تعمل الاستثناءات في وظائف التعليق تمامًا مثل الأخطاء في الدوال العادية. إذا تم عرض خطأ في وظيفة تعليق، سيتم إرساله إلى المتصل. وعلى الرغم من أن هذه الإعلانات يتم تنفيذها بشكل مختلف تمامًا، يمكنك استخدام حِزم محاولات/التقاط منتظمة للتعامل معها. ويُعد ذلك مفيدًا لأنه يتيح لك إمكانية الاعتماد على دعم اللغة المدمج للتعامل مع الأخطاء بدلاً من إنشاء معالجة أخطاء مخصصة لكل معاودة اتصال.
وإذا تخلصت من كوروتين، سيلغي هذا الكوروتين العنصر الرئيسي بشكل تلقائي. وهذا يعني أنه من السهل إلغاء العديد من المهام ذات الصلة معًا.
وبعد ذلك، في الجزء الأخير من الصورة، يمكننا التأكّد من أن مؤشر سريان العمل دائمًا بعد تشغيل طلب البحث.
شغِّل التطبيق مرة أخرى عن طريق اختيار إعدادات start ثم الضغط على . من المفترض أن يظهر لك مؤشر سريان التحميل عند النقر في أي مكان. لن يتغيّر العنوان لأنّنا لم نربط شبكتنا أو قاعدة بياناتنا بعد.
في التدريب التالي، عليك تحديث المستودع لتنفيذ العمل فعليًا.
في هذا التمرين، ستتعرّف على كيفية تبديل سلسلة المحادثات التي يتم تشغيل الكوروتين فيها من أجل تطبيق إصدار صالح من TitleRepository
.
مراجعة رمز معاودة الاتصال الحالي في refreshTitle
افتح TitleRepository.kt
وراجِع عملية التنفيذ الحالية المستندة إلى معاودة الاتصال.
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
في TitleRepository.kt
، يتم تنفيذ الطريقة refreshTitleWithCallbacks
من خلال معاودة الاتصال لإعلام التحميل وحالة الخطأ للمتصل.
تنفّذ هذه الوظيفة بعض الأمور لتنفيذ عملية التحديث.
- التبديل إلى سلسلة محادثات أخرى باستخدام
BACKGROUND
ExecutorService
- شغِّل طلب الشبكة
fetchNextTitle
باستخدام طريقة حظرexecute()
. سيؤدي هذا إلى تشغيل طلب الشبكة في سلسلة المحادثات الحالية، وفي هذه الحالة، تكون إحدى سلاسل المحادثات فيBACKGROUND
. - إذا كانت النتيجة ناجحة، احفظها في قاعدة البيانات باستخدام
insertTitle
واستدعِ طريقةonCompleted()
. - إذا لم تكن النتيجة ناجحة، أو إذا كان هناك استثناء، يُرجى الاتصال بالطريقة onError لإعلام المتصل بتعذّر إعادة التحميل.
هذا التنفيذ المستند إلى معاودة الاتصال آمن أساسي لأنه لن يحظر سلسلة المحادثات الرئيسية. ولكن يجب أن يستخدم معاودة الاتصال لإعلام المتصل عند اكتمال العمل. وتطلب أيضًا استدعاءات الاستدعاء في سلسلة محادثات BACKGROUND
التي تم تبديلها أيضًا.
إجراء مكالمات حظر من الكوروتين
وبدون إدخال الكوروتينات إلى الشبكة أو قاعدة البيانات، يمكننا جعل هذا الرمز آمنًا بشكل عام باستخدام الكوروتينات. سيتيح لنا ذلك التخلص من معاودة الاتصال والسماح لنا بتمرير النتيجة مرة أخرى إلى سلسلة المحادثات التي اتصلت بها في البداية.
ويمكنك استخدام هذا النمط في أي وقت تحتاج فيه إلى إجراء عمليات حظر أو عمل مكثّف لوحدة المعالجة المركزية (CPU) من داخل ممرّات معيّنة، مثل ترتيب قائمة كبيرة وفلترتها، أو القراءة من القرص.
للتبديل بين أي مُرسل، تستخدم الكوروتين withContext
. الاتصال بـ withContext
إلى المُرسل الآخر فقط لمصابيح لامدا ثم العودة إلى المُرسل الذي اتّصل به نتيجة لمدة.
وتوفّر الكوروتينات في لغة Kotlin تلقائيًا ثلاثة مكوّنات: Main
وIO
وDefault
. تم تحسين مرسِل IO للعمل من خلال القراءة من الشبكة أو القرص، في حين تم تحسين المُرسل التلقائي لأداء المهام التي تستهلك وحدة المعالجة المركزية (CPU).
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
يستخدم هذا التنفيذ حظر المكالمات للشبكة وقاعدة البيانات – ولكنه لا يزال أسهل قليلاً من إصدار معاودة الاتصال.
لا يزال هذا الرمز يستخدم المكالمات المحظورة. سيؤدي استدعاء execute()
وinsertTitle(...)
إلى حظر سلسلة المحادثات التي يتم فيها تشغيل هذا الكوروتين. ولكن من خلال التبديل إلى Dispatchers.IO
باستخدام withContext
، سنحظر أحد سلاسل المحادثات في المُرسل من خلال IO. سيتم تعليق الكورنيوم الذي استدعى ذلك، والذي من المحتمل أن يكون قيد التشغيل على Dispatchers.Main
، حتى اكتمال withContext
لامدا.
مقارنةً بإصدار رد الاتصال، هناك اختلافان مهمان:
- وتُرجع نتائج
withContext
إلى المُرسل الذي دعاها، في هذه الحالةDispatchers.Main
. إصدار معاودة الاتصال يسمى باستدعاءات الرسائل في سلسلة محادثات في خدمة منفّذBACKGROUND
. - وليس على المتصل ردّ استدعاء هذه الدالة. ويمكنهم الاعتماد على التعليق والاستئناف للحصول على النتيجة أو الخطأ.
تشغيل التطبيق مرة أخرى
وفي حال تشغيل التطبيق مرة أخرى، ستلاحظ أنّ تنفيذ السياسة الجديدة المستندة إلى الكوروتينات يتم تحميل النتائج من الشبكة.
في الخطوة التالية، ستدمج الكوروتينات في الغرفة وRetrofit.
لمواصلة تكامل الكوروتينات، سنستخدم دعم وظائف التعليق في الإصدار الثابت من الغرفة وRetrofit، ثم نبسِّط الرمز الذي كتبناه بشكل كبير عن طريق استخدام وظائف التعليق.
الكوروتينات في الغرفة
افتح MainDatabase.kt
أولاً واجعل insertTitle
وظيفة تعليق:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
عند إجراء ذلك، ستجعل غرفة طلبك طلب الآمن وستنفذه في سلسلة محادثات في الخلفية تلقائيًا. ومع ذلك، يعني هذا أيضًا أنه لا يمكنك استدعاء هذا الطلب إلا من داخل كوروتين.
وهذا كل ما عليك فعله لاستخدام الكوروتينات في الغرفة. رائع جدًا.
الكوروتينات في التحديث القديم
لنتعرّف بعد ذلك على كيفية دمج الكوروتينات مع التحديث القديم. فتح MainNetwork.kt
وتغيير fetchNextTitle
إلى وظيفة تعليق.
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
لاستخدام وظائف التعليق مع Retrofit، عليك اتخاذ إجراءين:
- إضافة معدِّل تعليق إلى الدالة
- أزِل برنامج تضمين
Call
من نوع الإرجاع. سنعرض هناString
، ولكن يمكنك عرض نوع معقّد يستند إلى json أيضًا. إذا كنت لا تزال تريد إتاحة الوصول إلىResult
بالكامل، يمكنك إرجاعResult<String>
بدلاً منString
من وظيفة التعليق.
ستجعل ميزة Retrofit تلقائيًا وظائف التعليق أساسية بحيث يمكنك الاتصال بها مباشرةً من Dispatchers.Main
.
استخدام الغرفة والحديث
الآن وبعد أن أصبحت وظائف غرفة الدردشة وRetrofit تتيح إمكانية تعليق الوظائف، يمكننا استخدامها من مستودعنا. يمكنك فتح TitleRepository.kt
والتعرّف على كيفية استخدام وظائف التعليق لتسهيل تبسيط المنطق، حتى مقارنة بإصدار الحظر:
العنوانRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
يا للروعة، هذا&'؛أقصر أصغر. ما السبب؟ اتضح أن الاعتماد على التعليق والاستئناف يجعل الترميز أقصر بكثير. تتيح لنا ميزة Retrofit استخدام أنواع الإرجاع مثل String
أو كائن User
هنا، بدلاً من Call
. هذا آمن، لأنّه خلال وظيفة التعليق، يتمكّن Retrofit
من تشغيل طلب الشبكة في سلسلة محادثات في الخلفية واستئناف كورون عند اكتمال المكالمة.
والأفضل من ذلك أننا تخلّصنا من withContext
. وبما أن كلاً من "الغرفة" و"Retrofit" يوفران وظائف تعليق آمنة رئيسية، فإنّه آمن من أجل تنظيم هذا العمل غير المتزامن من Dispatchers.Main
.
إصلاح أخطاء برامج التجميع
يتضمن الانتقال إلى الكوروتينات تغيير توقيع الدوال حيث لا يمكنك استدعاء دالة التعليق من دالة عادية. عند إضافة معدِّل suspend
في هذه الخطوة، تم إنشاء بعض أخطاء العارض الذي يُظهر ما سيحدث إذا غيّرت دالة ليتم تعليقها في مشروع حقيقي.
انتقل إلى المشروع وأصلح أخطاء برنامج التجميع عن طريق تغيير الدالة لتعليق التعليق الذي تم إنشاؤه. إليك حلول سريعة لكل منها:
TestingFakes.kt
حدِّث المنتجات المزيفة للاختبار لدعم مُعدِّلات التعليق الجديدة.
TitleDaoFake
- اضغط على Enter/إضافة مُعدِّلات تعليق لكل الوظائف في hehinchy
MainNetworkFake
- اضغط على Enter/إضافة مُعدِّلات تعليق لكل الوظائف في hehinchy
- استبدال
fetchNextTitle
بهذه الدالة
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- اضغط على Enter/إضافة مُعدِّلات تعليق لكل الوظائف في hehinchy
- استبدال
fetchNextTitle
بهذه الدالة
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- احذف الدالة
refreshTitleWithCallbacks
لأنها لم تعد مستخدمة.
تشغيل التطبيق
شغِّل التطبيق مرة أخرى، بمجرد تجميعه، سترى أنه جارٍ تحميل البيانات باستخدام الكوروتينات من طريقة العرض إلى Room وRetrofit.
تهانينا، لقد بدّلت هذا التطبيق باستخدام الكوروتينات بالكامل. وفي الختام، سنتحدث قليلاً عن كيفية اختبار ما أجريناه للتو.
في هذا التمرين، ستكتب اختبارًا يستدعي دالة suspend
مباشرةً.
بما أنّ نظام refreshTitle
تم الكشف عنه باعتباره واجهة برمجة تطبيقات عامة، سيتم اختباره مباشرةً، ويعرض كيفية استدعاء وظائف الكوروتينات من الاختبارات.
في ما يلي دالة refreshTitle
التي نفّذتها في آخر تمرين:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
كتابة اختبار استدعاء وظيفة التعليق
افتح TitleRepositoryTest.kt
في مجلد test
الذي يحتوي على اثنين من المهام.
حاوِل الاتصال بالرقم refreshTitle
من الاختبار الأول whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
بما أنّ الدالة refreshTitle
هي دالة suspend
، فإنّ لغة Kotlin لا تعرف كيفية طلبها إلا من فئة كورونتين أو وظيفة تعليق أخرى، لذا ستظهر لك علامة خطأ مجمّع، مثل "Suspend دالة updateTitle يجب استدعاءها من الكوروتين أو دالة تعليق أخرى فقط.
لا يعرف مُجري الاختبار أي شيء عن الكوروتينات حتى نتمكّن من جعل هذا الاختبار وظيفة تعليق. يمكننا استخدام الكوروتين في launch
باستخدام خاصية CoroutineScope
كما في ViewModel
، ولكن عليك إجراء الاختبارات كي تكتمل الكوروتينات قبل أن تعود. وبعد إرجاع دالة اختبارية، يكون الاختبار قد انتهى. الكوروتينات التي يتم بدؤها باستخدام launch
هي رموز غير متزامنة، وقد تكتمل في مرحلة ما في المستقبل. ولذلك، لاختبار هذا الرمز غير المتزامن، عليك إعداد طريقة ما حتى تنتظر من الاختبار حتى تكتمل كوروتين. بما أنّ launch
هي استدعاء لا يؤدي إلى الحظر، يعني هذا أنّها تعود على الفور ويمكن أن تستمر في تشغيل كوروتين بعد عرض الدالة. ولا يمكن استخدامها في الاختبارات. مثلاً:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
سيتعذّر هذا الاختبار أحيانًا. سيتم استدعاء المكالمة الصادرة إلى launch
على الفور ويتم تنفيذها في الوقت نفسه المتبقي من حالة الاختبار. لا يمكن للاختبار تحديد ما إذا كان refreshTitle
قد تم تشغيله بعد - وستكون أي تأكيدات مثل التحقق من تحديث قاعدة البيانات غير ثابتة. وإذا عرض refreshTitle
استثناءً، لن يتم طرحه في حزمة المكالمات التجريبية. بدلاً من ذلك، سيتم إنهاؤه إلى معالج الاستثناء غير المُدرج في GlobalScope
's.
تحتوي المكتبة kotlinx-coroutines-test
على الدالة runBlockingTest
التي تحظر أثناء استدعاء دوال التعليق. عندما يستدعي runBlockingTest
وظيفة تعليق أو launches
كوروتين جديدًا، ينفّذه على الفور بشكل تلقائي. ويمكنك اعتبارها طريقة لتحويل وظائف التعليق والكوروتينات إلى استدعاءات وظائف عادية.
بالإضافة إلى ذلك، سيعمل runBlockingTest
على إلغاء الاستثناءات غير الواضحة لك. يُسَهِّلُ هَذِهِ الْاخْتِبَارَاتْ عِنْدَمَا يَكُونُ الْكُرْيُوتِينْ مُرْتَفِعًا.
تنفيذ اختبار على كوروتين واحد
يمكن إنهاء المكالمة إلى refreshTitle
باستخدام runBlockingTest
وإزالة برنامج التضمين GlobalScope.launch
من subject.RefreshTitle().
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
يستخدم هذا الاختبار المنتجات المزيفة المقدمة للتحقّق من إدراج الرمز "&OK&OK&OK" في قاعدة البيانات بحلول refreshTitle
.
عندما يستدعي الاختبار runBlockingTest
، سيتم حظره حتى تكتمل الكوروتين الذي يبدأه runBlockingTest
. وبعد ذلك، أثناء استدعاء refreshTitle
، يستخدم نظام التعليق والاستئناف العادي للانتظار حتى تتم إضافة صف قاعدة البيانات إلى صفيفنا المزيف.
بعد اكتمال كوروتين الاختبار، يعود runBlockingTest
.
كتابة اختبار المهلة
نريد إضافة مهلة قصيرة إلى طلب الشبكة. لنكتب الاختبار أولاً ثم نفذ المهلة. إنشاء اختبار جديد:
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
يستخدم هذا الاختبار ميزة MainNetworkCompletableFake
المزيفة، وهي عبارة عن شبكة مزيفة مزيّفة ومصمّمة لتعليق المتصلين إلى أن يستمر الاختبار. عندما يحاول refreshTitle
إرسال طلب شبكة، سيتم تعليقه إلى الأبد لأننا نريد اختبار المهلة.
بعد ذلك، يطلق موسيقى كوروتي منفصلة لاستدعاء refreshTitle
. وتُعدّ هذه المهلة جزءًا رئيسيًا من مُهَل الاختبار، حيث يجب أن تحدث المهلة في نوع مختلف عن المدة التي ينشئها runBlockingTest
. وبذلك، يمكننا استدعاء السطر التالي، advanceTimeBy(5_000)
الذي سيتقدّم الوقت بمقدار 5 ثوانٍ ويؤدي إلى انتهاء مهلة السياسة الأخرى.
يُعد هذا اختبارًا كاملاً للمهلة، وسيمر هذا الاختبار بعد انتهاء المهلة المحدّدة.
يمكنك تشغيله الآن ومعرفة ما سيحدث:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
إحدى ميزات runBlockingTest
هي أنها لن تسمح لك بتسرب الكوروتينات بعد اكتمال الاختبار. وفي حال وجود أي الكوروتينات غير المكتملة، مثل الكوروتينات المُطلقة في نهاية الاختبار، لن تنجح في الاختبار.
إضافة مهلة
افتح TitleRepository
وأضِف مهلة خمس ثوانٍ إلى جلب الشبكة. يمكنك إجراء ذلك باستخدام الدالة withTimeout
:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
إجراء الاختبار. وعند إجراء الاختبارات، من المفترض أن يتم اجتياز كل الاختبارات.
في التمرين التالي، ستتعرّف على كيفية صياغة دوال الترتيب الأعلى باستخدام الكوروتينات.
في هذا التمرين، يمكنك إعادة ضبط refreshTitle
في MainViewModel
لاستخدام دالة تحميل البيانات العامة. ستعلّمك هذه الدورة التدريبية كيفية إنشاء وظائف ترتيب أعلى تستخدم الكوروتينات.
يعمل التنفيذ الحالي لـ refreshTitle
، ولكن يمكننا إنشاء متغيّر عام لتحميل البيانات يعرض دائمًا مؤشر سريان العمل. وقد يكون هذا مفيدًا في قاعدة الرمز التي تُحمِّل البيانات استجابة لعدة أحداث، وتريد ضمان عرض مؤشر سريان التحميل باستمرار.
إنّ مراجعة آلية التنفيذ الحالية لكل سطر، باستثناء repository.refreshTitle()
، تُعدّ نموذجًا نموذجيًا لعرض الأخطاء التي تظهر على اللوحة والدوّار.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
استخدام الكوروتينات في وظائف الترتيب الأعلى
إضافة هذا الرمز إلى MainViewmodel.kt
MainViewmodel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
أعِد ضبط refreshTitle()
الآن لاستخدام دالة الطلب الأعلى هذه.
MainViewmodel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
من خلال تجريد المنطق حول عرض مؤشر سريان العمل وعرض الأخطاء، سهّلنا الرمز الفعلي المطلوب لتحميل البيانات. إنّ عرض مؤشر سريان العمل أو عرض خطأ هو أمر بسيط يمكن تعميمه عند تحميل أي بيانات، بينما يجب تحديد مصدر البيانات والوجهة الفعليّة في كل مرة.
لإنشاء هذا الملخّص، يستخدم launchDataLoad
الوسيطة block
وهي عبارة عن lambda للتعليق. إنّ ميزة lambda للتعليق تتيح لك استدعاء وظائف التعليق. هذا هو كيفية تطبيق Kotlin لأدوات إنشاء الكوروتين launch
وrunBlocking
التي كنا نستخدمها في هذا الدرس التطبيقي حول الترميز.
// suspend lambda
block: suspend () -> Unit
ولصنع تعليق lambda، ابدأ بالكلمة الرئيسية suspend
. ويكمل سهم الدالة ونوع العرض Unit
التعريف.
ليس عليك في كثير من الأحيان الإعلان عن تعليق lambda، ولكن يمكن أن تكون مفيدة لإنشاء فكرة مثل التي تجسد المنطق المتكرر.
في هذا التمرين، ستتعرّف على كيفية استخدام الرمز المستنِد إلى الكوروتين من WorkManager.
ما هو المقصود بـ WorkManager؟
تتوفّر العديد من الخيارات على Android للعمل في الخلفية المؤجلة. يعرض لك هذا التمرين كيفية دمج WorkManager مع الكوروتين. WorkManager هو مكتبة متوافقة ومرنة وبسيطة للعمل في الخلفية المؤجلة. WorkManager هو الحل الموصى به لحالات الاستخدام هذه على Android.
WorkManager هو جزء من Android Jetpack، وهو أحد مكوِّنات الهندسة المعمارية لأعمال العمل في الخلفية التي تتطلب مزيجًا من فرص تنفيذية ومضمونة. ويعني التنفيذ الانتهازي أن "مدير العمل" سيتولى عملك في الخلفية في أقرب وقت ممكن. يعني التنفيذ المضمون أن WorkManager سيعتني بالمنطق لبدء عملك في مجموعة متنوعة من المواقف، حتى إذا انتقلت بعيدًا عن تطبيقك.
ولهذا السبب، يكون WorkManager خيارًا جيدًا للمهام التي يجب إكمالها في النهاية.
في ما يلي بعض الأمثلة على المهام التي تمثّل استخدامًا جيدًا لخدمة WorkManager:
- تحميل السجلات
- تطبيق الفلاتر على الصور وحفظ الصورة
- مزامنة البيانات المحلية مع الشبكة بصفة دورية
استخدام الكوروتينات في تطبيق WorkManager
يوفِّر تطبيق WorkManager عمليات تنفيذ مختلفة لفئته الأساسية ListanableWorker
في حالات الاستخدام المختلفة.
ويتيح لنا أبسط فئة عمل تنفيذ بعض العمليات المتزامنة من قِبل WorkManager. ومع ذلك، وبعد أن عملنا حتى الآن على تحويل قاعدة الرموز الخاصة بنا لاستخدام الكوروتينات ووظائف التعليق، فإن أفضل طريقة لاستخدام WorkManager هي من خلال صف CoroutineWorker
الذي يسمح بتحديد doWork()
الدالة كدالة تعليق.
للبدء، افتح RefreshMainDataWork
. وهي توسِّع نطاق CoroutineWorker
، وعليك تنفيذ doWork
.
داخل الدالة suspend
doWork
، يمكنك استدعاء refreshTitle()
من المستودع وعرض النتيجة المناسبة.
وبعد الانتهاء من إتمام المهام، سيبدو الرمز كما يلي:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
ويُرجى العِلم أنّ CoroutineWorker.doWork()
هي دالة تعليق. على عكس الفئة Worker
الأكثر بساطة، لا يتم تشغيل هذا الرمز على منفّذ أعمال الضبط المحدَّد في إعداد WorkManager، ولكنه يستخدم بدلاً من ذلك المُرسل في عضو coroutineContext
(بشكل تلقائي Dispatchers.Default
).
اختبار CoroutineWorker
يجب ألا يكون قاعدة الرموز كاملة بدون اختبار.
يوفّر تطبيق ManagerManager بعض الطرق المختلفة لاختبار صفوف Worker
. لمعرفة المزيد من المعلومات عن البنية الأساسية للاختبار الأصلي، يمكنك قراءة المستندات.
ويقدّم الإصدار 2.1 من WorkManager مجموعة جديدة من واجهات برمجة التطبيقات لإتاحة طريقة أبسط لاختبار صفوف ListenableWorker
، ونتيجة لذلك، يستخدم تطبيق CoroutineWorker. في الرمز الخاص بنا، سنستخدم أحد واجهات برمجة التطبيقات الجديدة التالية: TestListenableWorkerBuilder
.
لإضافة اختبارنا الجديد، عدِّل ملف RefreshMainDataWorkTest
ضمن المجلد androidTest
.
يكون محتوى الملف هو:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
قبل البدء في الاختبار، نخبر WorkManager
عن المصنع كي نتمكن من إدخال الشبكة المزيفة.
يستخدم الاختبار نفسه TestListenableWorkerBuilder
لإنشاء العامل الذي يمكننا بعد ذلك إجراء التشغيل باستخدام طريقة startWork()
.
وتُعدّ أداة WorkManager مثالاً واحدًا على كيفية استخدام الكوروتينات لتبسيط تصميم واجهات برمجة التطبيقات.
في هذا الدرس التطبيقي حول الترميز، تناولنا الأساسيات التي يجب أن تبدأ استخدامها في الكوروتين في تطبيقك.
تناولنا ما يلي:
- كيفية دمج الكوروتينات في التطبيقات المتوافقة مع Android من وظائف واجهة المستخدم وWorkManager لتبسيط البرمجة غير المتزامنة،
- طريقة استخدام الكوروتينات في
ViewModel
لجلب البيانات من الشبكة وحفظها في قاعدة بيانات بدون حظر سلسلة المحادثات الرئيسية. - وطريقة إلغاء كل الكوروتينات عند انتهاء
ViewModel
بالنسبة إلى اختبار الترميز الذي يعتمد على الكوروتين، تناولنا كلاً من اختبار الأداء وكذلك استدعاء دوال suspend
مباشرةً من الاختبارات.
مزيد من المعلومات
يمكنك الاطّلاع على تطبيق الكوروتينات المتطوّرة باتّباع خطوات Kotlin Flow وLiveData&code; للتعرّف على مزيد من المعلومات حول استخدام الكوروتينات على نظام التشغيل Android.
تحتوي الكوروتينات بلغة Kotlin على العديد من الميزات التي لم يتناولها هذا الدرس التطبيقي حول الترميز. وإذا كنت مهتمًا بمعرفة المزيد من المعلومات عن الكوروتينات في لغة Kotlin، يمكنك الاطّلاع على أدلة الكوروتين المنشورة من خلال JetBrains. يمكنك أيضًا الاطّلاع على &brbr;تحسين أداء التطبيق باستخدام Kotlin coroutines" لمزيد من أنماط استخدام الكوروتين على Android.