Kotlin coroutines

Sanjar Suvonov
9 min readAug 9, 2021

--

Kotlin Korotinlar

  • Korotinlar o’zi nma?
  • Nega Kotlin korotinlar beradigan yechimlarga ehtiyoj bor?
  • Korotinlarni Androidda ishlatish uchun qadamma qadam qo’llanma.
  • Kotlin Korotinlarda “Scope”lar nma?
  • Kotlin Korotinlarda Exeption’lar bilan qanday ishlanadi?
  • Androidda Kotlin Korotinlarni o’rganish uchun proyektlar.

Korotinlar o’zi nma?

Coroutines = Co + Routines

Bu yerda Co — kooperatsiya degan ma’noni anglatadi va Routines — funktsiyalarni anglatadi.

Ya’ni, funktsiyalar bir-biri bilan hamkorlik qilganda, biz ularni Coroutines(Korotinlar) deb ataymiz.

Keling, buni bir misol bilan o’rganamiz. Tushunish uchun quyida keltirilgan kodni boshqacha tarzda yozdim. Bizda functionA va functionB kabi ikkita funktsiya bor deylik.

Quyida functionA:

fun functionA(case: Int) {
when (case) {
1 -> {
taskA1()
functionB(1)
}
2 -> {
taskA2()
functionB(2)
}
3 -> {
taskA3()
functionB(3)
}
4 -> {
taskA4()
functionB(4)
}
}
}

Va functionB:

fun functionB(case: Int) {
when (case) {
1 -> {
taskB1()
functionA(2)
}
2 -> {
taskB2()
functionA(3)
}
3 -> {
taskB3()
functionA(4)
}
4 -> {
taskB4()
}
}
}

So’ngra biz functionA ni bu tarzda ishlatamiz:

functionA(1)

Bu yerda functionA taskA1 ni bajaradi va muvozanatni functionBga taskB1 bajarish uchun beradi.

Keyin functionB taskB1 vazifasini bajaradi va taskA2 vazifasini bajarish uchun boshqaruvni functionA funktsiyasiga qaytaradi va hokazo.

Muhimi shundaki, functionA va functionB bir-biri bilan hamkorlikda ishlaydi.

Kotlin Korotinlar bilan yuqoridagi hamkorlik when yoki switch case larni ishlatmasdan juda osonlik bilan amalga oshiriladi.

Endi, biz funktsiyalar o’rtasidagi hamkorlik haqida gap ketganda Korotinlar nima ekanligini tushunib yetdik. Funksiyalarni birgalikda ishlay olish jihati tufayli ochiladigan cheksiz imkoniyatlar mavjud.

Quyida bir nechtasini keltirib o’tamiz:

  • U functionA ning bir necha qator kodlarini ishga tushirib, so’ngra functionB ning bir necha qator amallarini bajarishi mumkin, va so’ngra yana functionAning ma’lum bir amallarini bajarib va boshqa funktsiyaga o’tib ketishi mumkin. Bu oqimning hech qanday vazifa bajarmayotgan holatida boshqa bir funksiyalarning ayrim qatorlarini ishlatish uchun foydali bo’ladi. Xullas hamkorlikda ishlash bir necha vazifani bir vaqtni o’zida bajarishga yordam beradi.
  • U asinxron(bir vaqtni o’zida yozib bo’lmaydigan) kodlarni sinxron yozishga zamin yaratadi. Bu haqida keyinroq maqolamizda batafsil suhbatlashamiz.

Xo’sh shunda biz Korotinlar va oqimlarni har ikkisini ko’p vazifali deya olamiz. Ammo farq shundaki, oqimlar OS(Operatsion Sistema) tomonidan, Korotinlar esa foydalanuvchilar tomonidan boshqarilganligi sababli hamkorlikda ish

Bu funktsiyalarning kooperativ xususiyatidan foydalanib, uni yengil va shu bilan birga kuchli qilish uchun haqiqiy oqimlar ustida yozilgan optimal framework. Shunday qilib, biz Korotinlarni — yengil oqimlar deb atashimiz mumkin. Yengil oqim asl oqimda joylashmaydi, bu protsessorda muhit almashtirishni talab qilmaydi, shuning uchun ular tezroq ishlaydi.

“Yengil oqim asl oqimda joylashmaydi” degani nimani anglatadi?

Korotinlar ko’plab tillarda mavjud. Asosan, Korotinlarning ikki turi mavjud:

  • Stackless(To’plamsiz)
  • Stackful(To’plamli)

Kotlin to’plamsiz Korotinlarni ishlatadi — bu shuni anglatadiki, Korotinlarning o’z to’plamlari yo’q, shuning uchun ular asl oqimda joylashmaydilar.

Korotinlar oqimlarning o’rnini egallamaydilar balki ularni boshqaruvchi bir framerwork’dir.

Nega Kotlin korotinlar beradigan yechimlarga ehtiyoj bor?

Keling, Android ilovasining juda oddiy holatida ko’rib chiqaylik:

  • Serverdan Userni olib kelish.
  • Userni UIda ko’rsatish.
fun fetchUser(): User {
// make network call
// return user
}
fun showUser(user: User) {
// show user
}
fun fetchAndShowUser() {
val user = fetchUser()
showUser(user)
}

fetchAndShowUser funktsiyasini ishlatganimizda, u NetworkOnMainThreadExceptionni tashlaydi, chunki asosiy oqimda tarmoq chaqiruviga yo’l qo’yilmaydi.

Buni hal qilishning ko’plab usullari mavjud. Ulardan bir nechtasi quyidagicha:

  1. Callback’dan foydalanish: Bu yerda biz fetchUserni oqimda ishga tushiramiz va natijani callback bilan uzatamiz.
  2. RxJava’dan foydalanish: reaktiv yondashuv. Shu tarzda biz ichki callbackdan qutulishimiz mumkin.
fetchUser()
.subscribeOn(Schedulers.io())
.observerOn(AndroidSchedulers.mainThread())
.subscribe { user ->
showUser(user)
}

3. Korotinlar’dan foydalanish: Ha, korotinlar.

suspend fun fetchAndShowUser() {
val user = fetchUser() // fetch on IO thread
showUser(user) // back on UI thread
}

Bu erda yuqoridagi kod sinxron ko’rinadi, ammo u asinxrondir. Qanday qilib buning iloji borligini ko’rib chiqamiz.

Kotlin Korotinlarni Androidda ishlatish

Android loyihasidagi Kotlin Korotinlar kutubxonasini quyidagi tarzda qo’shing:

dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Endi fetchUser funktsiyamiz quyidagi ko’rinishga ega bo’ladi:

suspend fun fetchUser(): User {
return GlobalScope.async(Dispatchers.IO) {
// make network call
// return user
}.await()
}

Xavotir olmang biz hali suspend, GlobalScope, async, await, va Dispatchers.IO’larni birma bir o’rganib chiqamiz.

fetchAndShowUser funktsiyasi esa quyida:

suspend fun fetchAndShowUser() {
val user = fetchUser() // fetch on IO thread
showUser(user) // back on UI thread
}

Va quyida ko’rsatilgan showUser funktsiyasi xuddi avvalgidek:

fun showUser(user: User) {
// show user
}
  • Dispatchers: Dispetcherlar ish bajarilishi kerak bo’lgan oqimni tanlashda korotinlarga yordam beradi. Dispetcherlarning asosan IO, Default va Main kabi uchta turi mavjud. IO — dispetcher tarmoq va disk bilan bog’liq ishlarni bajarish uchun ishlatiladi. Default — protsessorni intensiv ishlarda bajarish uchun ishlatiladi. Main — Androidni UI oqimi. Ulardan foydalanish uchun ishni async funktsiyasi bilan boshlaymiz. Async funktsiyasi quyida keltirilgan.
suspend fun async()
  • suspend: Suspend funktsiyasi — bu boshlash, to’xtatib turish va davom ettirish mumkin bo’lgan funktsiya.
Rasmda funtionAni bir oqimdan ikkinchi oqimga o’tganda to’xtatib, yana keyingisida davom ettirilishi ko’rsatilgan.

Suspend funktsiyalarini faqat Korotin yoki boshqa suspend funktsiyasidan chaqira olasiz. Async funktsiyasidan oldida suspend kaliti turganini ko’rishingiz mumkin va bundan foydalanish uchun ham biz bu funksiyani Suspend qilishimiz kerak.

Endi, fetchAndShowUser faqatgina boshqa suspend funktsiyasi yoki Korotin orqali chaqirilishi mumkin. Biz activity’ning onCreate funktsiyasini suspend(to’xtatib turolmaymiz), shuning uchun biz uni quyidagi Korotinlar bilan chaqirishimiz kerak:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch(Dispatchers.Main) {
fetchAndShowUser()
}
}

Ya’ni:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch(Dispatchers.Main) {
val user = fetchUser() // fetch on IO thread
showUser(user) // back on UI thread
}
}

showUser UI oqimida ishlaydi, chunki biz uni ishga tushirish uchun Dispatchers.Main dan foydalanganmiz.

Korotinlarni boshlash uchun Kotlinda ikkita funktsiya bor:

  • launch{}
  • async{}

Launch vs Async

Farqi shundaki, launch{} hech narsa qaytarmaydi va async{} o’zida await() funktsiyasi bor bo’lgan Deferred<T> ni qaytaradi. Await() funktsiyasi Korotinning natijasini qaytarib, bu Javada natija olish uchun future.get() ishlatishga o’xshaydi.

Boshqacha qilib aytganda:

  • launch: ishga tushuradi va unutadi
  • async: vazifani bajaradi va natija qaytaradi

Keling launch va async ni o’rganish uchun misollar ko’ramiz:

suspend fun fetchUserAndSaveInDatabase() {
// user ni internetdan olib keladi
// user ni database ga saqlaydi
// va hech nma qaytarmaydi
}

Endi biz launch ni quyidagi kabi ishlatishimiz mumkin:

GlobalScope.launch(Dispatchers.Main) {
fetchUserAndSaveInDatabase() // do on IO thread
}

fetchUserAndSaveInDatabase hech narsa qaytarmagani uchun biz ushbu vazifani launch orqali tugallashimiz va Main Thread(Asosiy Oqim)da boshqa ishlarni qilishimiz mumkin.

Va qachonki biz natija olishimiz kerak bo’lsa, bizga async ni ishlatish qo’l keladi.

Bizning ikkita funktsiyamiz bor, ular User ni quyidagicha qaytaradi:

suspend fun fetchFirstUser(): User {
// tarmoqga murojat qilib
// user ni qaytaradi
}
suspend fun fetchSecondUser(): User {
// tarmoqga murojat qilib
// user ni qaytaradi
}

Endi, biz async dan quyidagi kabi foydalanishingiz mumkin:

GlobalScope.launch(Dispatchers.Main) {
val userOne = async(Dispatchers.IO) { fetchFirstUser() }
val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
showUsers(userOne.await(), userTwo.await()) // back on UI thread
}

Bu yerda async ikkala tarmoq chaqiruvini parallel ravishda amalga oshiradi, natijalarni kutadi va keyin showUsers funktsiyasini ishlatadi.

Shunday qilib, endi launch funktsiyasi bilan async funktsiyasi o’rtasidagi farqni angladik.

Bizda yana withContext deb nomlanga tushuncha bor

suspend fun fetchUser(): User {
return GlobalScope.async(Dispatchers.IO) {
// make network call
// return user
}.await()
}

withContext bu await() ning o’rniga qo’llanilishi mumkin funktsiya.

suspend fun fetchUser(): User {
return withContext(Dispatchers.IO) {
// make network call
// return user
}
}

Lekin bizda hali withContext va await haqida bilishimiz kerak bo’lgan yana ko’p narsalar mavjud.

Keling endi biz async ning fetchFitstUser va fetchSecondUser misollarida withContext ni ishlatib ko’ramiz.

GlobalScope.launch(Dispatchers.Main) {
val userOne = withContext(Dispatchers.IO) { fetchFirstUser() }
val userTwo = withContext(Dispatchers.IO) { fetchSecondUser() }
showUsers(userOne, userTwo) // back on UI thread
}

Qachonki biz withContext dan foydalansak, u parallel ishlashni o’rniga ketma ketlikda ishlaydi va bu juda katta farqdir.

Eslatmalar:

  • Parallel ish bajarish ehtiyoj bo’maganda, withContext dan foydalaning.
  • Qachonki parallel muhit kerak bo’lsa async ni ishlating.
  • launch bilan olinishi mumkin bo’lmagan natijani withContext va async ishlatish orqali olinadi.
  • withContext ishlatish orqali bir dona vazifadan natija olinadi.
  • async ishlatish bilan esa bir nechta vazifalar natijalari parallel muhitda olinadi.

Kotlin Korotinlarda Scope’lar

Scope lar Kotlin Korotinlarda shunchalik foydaliki, biz ularni activity destroy(to’xtashi) bo’lishi bilan orqa fonda ishlab turgan vazifalarni bekor qilish uchun ishlatamiz. Bu erda biz ushbu turdagi vaziyatlarni boshqarish uchun scopelardan qanday foydalanishni bilib olamiz.

Activityni scope deb hisoblasak, activity buzilishi bilanoq orqa fondagi vazifa bekor qilinishi kerak.

Biz activityda CoroutineScopeni implement qilib olishimiz kerak.

class MainActivity : AppCompatActivity(), CoroutineScope {    override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private lateinit var job: Job
}

onCreate va onDestroy funkstiyasida:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job() // create the Job
}
override fun onDestroy() {
job.cancel() // cancel the Job
super.onDestroy()
}

endi launchni quyidagi kabi ishlatamiz:

launch {
val userOne = async(Dispatchers.IO) { fetchFirstUser() }
val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
showUsers(userOne.await(), userTwo.await())
}

Agar vazifa ishlayotgan bo’lsa, Activity Destroy qilinishi bilanoq u ham bekor qilinadi chunki biz scopeni berib qo’ydik.

Qachonki bizga activityning scope emas, balki applicationning GlobalScope kerak bo’lsa uni quyidagi kabi ishlatamiz:

GlobalScope.launch(Dispatchers.Main) {
val userOne = async(Dispatchers.IO) { fetchFirstUser() }
val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
}

Endi bu yerda xatto activity destroy bo’lganda ham fetchSecondUser funktsiyalari ishlashda davom etadi, chunki biz GlobalScope ni ishlatdik.

Mana nega Scopelar Kotlin Korotinlarda juda foydali.

Kotlin Korotinlarda Exeption’lar bilan qanday ishlanadi?

Exeptionlar dan foydalanish yana bir muhim mavzudir. Biz buni o’rganishimiz kerak.

Qachonki Launch’ni ishlatganimizda

Birinchi yo’l try-catch ni ishlatish

GlobalScope.launch(Dispatchers.Main) {
try {
fetchUserAndSaveInDatabase()
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}

Ikkinchi yo’li handler bilan

Buning uchun quyidagi kabi exception handler yaratishimiz kerak:

class MainActivity : AppCompatActivity(), CoroutineScope {override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
private lateinit var job: Job
}

Va quyidagi kabi ishlatamiz:

launch {
fetchUserAndSaveInDatabase()
}

Qachonki Async’ni ishlatganimizda

Asyncdan foydalanganda biz quyidagi kabi exceptionlar bilan ishlash uchun try-catch blokidan foydalanishimiz kerak.

val deferredUser = GlobalScope.async {
fetchUser()
}
try {
val user = deferredUser.await()
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !") //exeption ushlandi
}

Keling, Android Development-da istisnolardan foydalanishning real uchrashi mumkin bo’lgan holatlarini ko’rib chiqamiz.

Aytaylik, bizda quyidagi ikkita tarmoq chaqiruvlari bor:

  • getUsers()
  • getMoreUsers()

Va biz tarmoq chaqiruvlarini quyidagi ketma ketlikda beramiz:

launch { 
try {
val users = getUsers()
val moreUsers = getMoreUsers()
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !") //exeption ushlandi
}
}

Agar bu yerdagi biror bir chaqiruv ishlamay qolsa va xatolik bo’lsa u to’gridan to’g’ri catch blokiga boradi.

Deylik, biz xatolik bilan tugagan tarmoq chaqiruvi uchun bo’sh listni qaytarishni va boshqa tarmoq chaqiruvlarini javobini davom ettirmoqchi bo’lsakchi. Biz quyidagi kabi har bir tarmoq chaqiruviga alohida catch blokini berib qo’ya olamiz:

launch { 
try {
val users = try {
getUsers()
} catch (e: Exception) {
emptyList<User>()
}
val moreUsers = try {
getMoreUsers()
} catch (e: Exception) {
emptyList<User>()
}
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !") //exception ushlandi
}
}

Shu tarzda, biron bir xato bo’lsa, u bo’sh list bilan davom etadi.

Endi biz tarmoq chaqiruvlarini parallel ravishda amalga oshirishni xohlasak nima bo’ladi. Async yordamida quyidagi kodni yozishimiz mumkin.

launch { 
try {
val usersDeferred = async { getUsers() }
val moreUsersDeferred = async { getMoreUsers() }
val users = usersDeferred.await()
val moreUsers = moreUsersDeferred.await()
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}

Bu yerda biz bitta muammoga duch kelamiz, agar biron bir tarmoq xatosi kelib chiqsa, dastur ishdan chiqadi! U catch blokiga O’TMAYDI.

Buni yechish uchun coroutineScopedan quyidagi kabi foydalanishimiz kerak bo’ladi:

launch { 
try {
coroutineScope {
val usersDeferred = async { getUsers() }
val moreUsersDeferred = async { getMoreUsers() }
val users = usersDeferred.await()
val moreUsers = moreUsersDeferred.await()
}
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !") //exeption ushlandi
}
}

Endi, agar biron bir tarmoq xatosi kelib chiqsa, u catch blokiga o’tadi.

Ammo yana bir bor deylik, biz muvaffaqiyatsiz tugagan tarmoq qo’ng’irog’i uchun bo’sh listni qaytarib, boshqa tarmoq chaqiruvining javobi bilan davom etmoqchimiz. Biz supervisorScopedan foydalanishimiz va quyidagi kabi har bir tarmoq chaqiruviga alohida catch blokini berishimiz kerak bo’ladi:

launch { 
try {
supervisorScope {
val usersDeferred = async { getUsers() }
val moreUsersDeferred = async { getMoreUsers() }
val users = try {
usersDeferred.await()
} catch (e: Exception) {
emptyList<User>()
}
val moreUsers = try {
moreUsersDeferred.await()
} catch (e: Exception) {
emptyList<User>()
}
}
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}

Lekin shunga qaramay, agar biron bir xato bo’lsa, yana u o’sha bo’sh list bilan davom etadi.

Mana supervisorScope qanday yordam bera oladi.

Xulosa

  • async ni ISHLATMAGAN vaqtimiz, biz bemalol try-catch yoki CoroutineExceptionHandler orqali deyarli hammar narsaga erishishimiz mumkin.
  • async dan FOYDALANGANIMIZDA try-catchga qo’shimcha o’laroq, bizda ikkiita imkoniyat bor: coroutineScope va supervisorScope asyncda, biz supervisorScope ni:

1. har bir vazifa bilan ishlaydigan try-catch
2. biri ishlamay qolsa boshqasiga ko’chadigan mukammalroq try-catch bilan ishlata olamiz.

  • async bilan coroutineScope ni biror bir task bilan ishlamaganda boshqasiga o’tish uchun ishlatiladigan try-catchdan qochish uchun foydalanishingiz mumkin.

Asosiy farq shundaki, coroutineScope har qanday ichki funktsiyasi xatolikka uchraganida uni umuman bekor qiladi. Agar biz xatolikka uchragan taqdirda ham boshqa vazifalarni davom ettirishni xohlasak, biz supervisorScope bilan ishlaymiz. SupervisorScope ichki funktsiyasi xatolikka uchraganda uni bekor qilmaydi.

Mana Kotlin Korotinlarda Exeptionlar bilan qanday ishlanadi.
O’ylaymanki, bugun biz o’zimiz uchun juda yaxshi bilim oldik. E’tiboringiz uchun katta rahmat :)

Keling, Kotlin Korotinlardan foydalanishni boshlaymiz.

Androidda Kotlin Korotinlarni ishlatishni misollar orqali o’rganing.

Maqola quyidagi link asosida tayyorlandi.

--

--