ما يجب معرفته قبل البدء باستخدام Coroutine على Android
باختصار، Coroutine طريقة لكتابة كود غير متزامن (Asynchronous) بأسلوب متزامن (Synchronous) مباشر، دون الحاجة إلى كومة من الـ callbacks.

لا بد أن كل مطوّر Android قد سمع عن Coroutine الآن. فخلال السنوات القليلة الماضية، اكتسبت Coroutine شعبية كبيرة في مجتمع Android إلى جانب Kotlin. في هذا المقال سأتحدث عن أساسيات Coroutine التي تمنيت لو عرفتها حين بدأت استكشافها. باختصار، Coroutine طريقة لكتابة كود غير متزامن (Asynchronous) بأسلوب متزامن (Synchronous) مباشر، دون الحاجة إلى كومة من الـ callbacks. لن نضيّع الوقت. إليكم مثالًا على كيفية استخدام Coroutine.
class ArtistViewModel(
private val getArtistsUseCase: GetArtistsUseCase
) : ViewModel(), CoroutineScope {
private val job = Job()
val artistsLiveData: MutableLiveData<List<Artist>> = MutableLiveData()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
fun getArtist() {
launch {
val artistResponse = withContext(Dispatchers.IO) {
getArtistsUseCase("Slayer")
}
artistsLiveData.value = artistResponse?.result?.map {
Artist(it.name, it.genreName)
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
في هذا الكود لدينا class من نوع ViewModel. نستخدم Coroutine لجلب قائمة كائنات Artist من الـ API عبر getArtistUseCase. وقد يُعدّ هذا الأسلوب في استخدام Coroutine مع ViewModel قديمًا بعض الشيء، فهناك طريقة أسهل، لكن لنؤجلها قليلًا. قبل المتابعة، سأشرح المصطلحات المهمة التالية.
-
Coroutine context يحدد كيفية تنفيذ الـ Coroutines، مثل تشغيلها على UI Thread أو على IO Thread. يتكوّن Coroutine Context من عناصر مختلفة، أهمها عنصران: الـ Job الخاص بالـ Coroutine والـ Dispatcher الخاص بها- Job من حيث المفهوم شيء قابل للإلغاء، ويمكن تنظيمه في تسلسل هرمي من الآباء والأبناء. وإلغاء الأب يؤدي إلى إلغاء جميع أبنائه.
-
Dispatchers تحدد الـ thread (أو الـ threads) التي ستُنفَّذ عليها الـ coroutine المعنية. وهناك نوعان هما الأكثر استخدامًا: Dispatchers.Main نستخدمه حين نريد تشغيل الـ Coroutine على UI Thread، بينما Dispatchers.IO مخصص للـ Coroutine التي تحجب الـ thread الحالي، مثل استدعاء خدمة API أو الوصول إلى قواعد البيانات أو الملفات.
-
Coroutine Builder يُستخدم لإنشاء Coroutine جديدة. وهناك ثلاثة builders رئيسية.
- launch؛ ربما يكون launch الأكثر استخدامًا لأنه أسهل طريقة لإنشاء Coroutine. والـ Coroutines التي ينشئها
launchلا تحجب الـ thread الجاري. - async؛ وهو builder يستطيع إرجاع قيمة من داخل الـ Coroutine.
- runBlocking؛ هذا الـ builder عكس launch، لأنه يحجب الـ Thread الجاري. وستستخدمونه غالبًا عند كتابة اختبارات الـ Coroutines.
- launch؛ ربما يكون launch الأكثر استخدامًا لأنه أسهل طريقة لإنشاء Coroutine. والـ Coroutines التي ينشئها
-
CoroutineScope يحدد نطاقًا (scope) للـ coroutines الجديدة. والـ Coroutine builder (مثل launch و async وغيرهما) هو extension على CoroutineScope، ويرث منه CoroutineContext لينقل جميع عناصره وعمليات الإلغاء تلقائيًا.
لنعد إلى مثالنا. سننشئ Coroutine لجلب البيانات من خدمة API، ولنفصّل ذلك خطوة بخطوة.
لإنشاء Coroutine، نحتاج إلى تحديد Coroutine Context للـ Coroutine Builder.
class ArtistViewModel(
private val getArtistsUseCase: GetArtistsUseCase
): ViewModel(), <strong>CoroutineScope</strong> {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
...
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
نجعل الـ class ArtistViewModel يطبّق CoroutineScope، وهذا يلزمنا أيضًا بعمل override لـ Coroutine Context.
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
يعني الجزء أعلاه أن الـ Dispatcher الافتراضي للـ Coroutine في هذا الـ Class هو Dispatchers.Main، وأن job يتيح لنا التحكم في إلغاء الـ Coroutine داخل هذا الـ class عبر استدعاء **job.cancel()**في المثال، نستدعي job.cancel() داخل onCleared() حتى تتوقف الـ Coroutines إذا أُتلف الـ viewModel. لننتقل الآن إلى الجزء الذي تُنشأ فيه الـ Coroutine.
launch {
val response = <strong>withContext</strong>(Dispatchers.IO) {
getArtistsUseCase("Slayer")
}
artistsLiveData.value = response?.result
}
بما أننا نحتاج إلى استدعاء خدمة API، فهكذا ننتقل من التنفيذ على الـ Main thread إلى الـ IO thread. نستخدم withContext للتبديل من Dispatcher.Main إلى Dispatcher.IO، وسيُنفَّذ الكود داخل كتلة withContext على الـ IO thread. ويمكن تطبيق المفهوم نفسه على مكونات أخرى غير ViewModel، مثل Presenter أو Activity أو Fragment.
في الواقع، مع ViewModel في مكتبات AndroidX lifecycle v2.1.0، يتوفر viewModelScope الذي يمثل نطاق Coroutine الخاص بالـ ViewModel والمرتبط بدورة حياته. لذلك لم نعد بحاجة إلى استدعاء job.cancel() داخل onClear(). إليكم المثال التالي.
class ArtistViewModel(
private val getArtistsUseCase: GetArtistsUseCase
) : ViewModel() {
val artistsLiveData: MutableLiveData<List<Artist>> = MutableLiveData()
fun getArtist() {
viewModelScope.launch {
val artistResponse: ResponseWrapper<ArtistApi>? = withContext(Dispatchers.IO) {
getArtistsUseCase("Slayer")
}
artistsLiveData.value = artistResponse?.result?.map {
Artist(it.name, it.genreName)
}
}
}
}
يمكنكم أيضًا الاطلاع على الكود في المستودع الخاص بي.
- بدون viewModelScope: https://github.com/BenBoonya/simple-coroutine-in-android-demo
- مع viewModelScope: https://github.com/BenBoonya/simple-coroutine-in-android-demo/tree/viewmodel-scope
المصدر
صورة غلاف المقال مأخوذة من العمل الفني لألبوم Fear Inoculum لفرقة Tool.


