در این لبه کد یاد خواهید گرفت که چگونه از Kotlin Coroutines در یک برنامه اندرویدی استفاده کنید—روشی جدید برای مدیریت رشته های پس زمینه که می تواند کد را با کاهش نیاز به تماس ها ساده کند. Coroutineها یک ویژگی 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، از جمله توابع پسوند و لامبدا.
- درک اولیه از استفاده از رشته ها در اندروید، از جمله رشته اصلی، رشته های پس زمینه و تماس ها.
کاری که خواهی کرد
- با کدهای نوشته شده تماس بگیرید و نتایج را بدست آورید.
- از توابع تعلیق برای متوالی کردن کدهای همگام سازی استفاده کنید.
- از
launch
وrunBlocking
برای کنترل نحوه اجرای کد استفاده کنید. - با استفاده از suspendCoroutine، تکنیکهایی را برای تبدیل APIهای موجود به
suspendCoroutine
. - از کوروتین ها با کامپوننت های معماری استفاده کنید.
- بهترین روش ها را برای آزمایش کوروتین ها بیاموزید.
آنچه شما نیاز دارید
- Android Studio 3. 5 (ممکن است نرم افزار کد با نسخه های دیگر کار کند، اما ممکن است برخی چیزها گم شده باشند یا متفاوت به نظر برسند).
اگر در حین کار با این کد با مشکلاتی (اشکالات کد، خطاهای دستوری، عبارت نامشخص و غیره) مواجه شدید، لطفاً مشکل را از طریق پیوند گزارش یک اشتباه در گوشه سمت چپ پایین صفحه کد گزارش کنید.
کد را دانلود کنید
برای دانلود تمامی کدهای این کد لبه روی لینک زیر کلیک کنید:
... یا با استفاده از دستور زیر مخزن GitHub را از خط فرمان کلون کنید:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
سوالات متداول
ابتدا، بیایید ببینیم که برنامه نمونه شروع به چه شکلی است. برای باز کردن نمونه برنامه در Android Studio این دستورالعمل ها را دنبال کنید.
- اگر فایل فشرده
kotlin-coroutines
را دانلود کردید، فایل را از حالت فشرده خارج کنید. - پروژه
coroutines-codelab
را در Android Studio باز کنید. - ماژول
start
برنامه را انتخاب کنید. - کلیک کنید بر روی
دکمه اجرا را انتخاب کنید یا یک شبیه ساز انتخاب کنید یا دستگاه اندرویدی خود را که باید قابلیت اجرای Android Lollipop را داشته باشد وصل کنید (حداقل SDK پشتیبانی شده 21 است). صفحه Kotlin Coroutines باید ظاهر شود:
این برنامه شروع کننده از رشتهها برای افزایش شمارش با تأخیر کوتاهی پس از فشار دادن صفحه استفاده میکند. همچنین عنوان جدیدی را از شبکه دریافت می کند و آن را روی صفحه نمایش می دهد. اکنون آن را امتحان کنید و بعد از یک تاخیر کوتاه، تعداد و پیام تغییر می کند. در این کد لبه شما این برنامه را به استفاده از کوروتین ها تبدیل می کنید.
این برنامه از کامپوننتهای معماری برای جدا کردن کد UI در MainActivity
از منطق برنامه در MainViewModel
استفاده میکند. یک لحظه برای آشنایی با ساختار پروژه وقت بگذارید.
-
MainActivity
رابط کاربری را نمایش می دهد، شنوندگان کلیک را ثبت می کند و می تواند یکSnackbar
را نمایش دهد. رویدادها را بهMainViewModel
می کند و صفحه را بر اساسLiveData
درMainViewModel
به روز می کند. -
MainViewModel
رویدادها را درonMainViewClicked
مدیریت می کند و با استفاده ازLiveData.
باMainActivity
ارتباط برقرار می کند. -
Executors
BACKGROUND,
را تعریف می کنند که می تواند چیزها را روی یک رشته پس زمینه اجرا کند. -
TitleRepository
نتایج را از شبکه واکشی می کند و آنها را در پایگاه داده ذخیره می کند.
اضافه کردن کوروتین ها به پروژه
برای استفاده از coroutines در Kotlin، باید کتابخانه coroutines-core
را در build.gradle (Module: app)
پروژه خود قرار دهید. پروژه های Codelab قبلاً این کار را برای شما انجام داده اند، بنابراین برای تکمیل کد لبه نیازی به انجام این کار ندارید.
Coroutine ها در Android به عنوان یک کتابخانه اصلی و برنامه های افزودنی خاص Android در دسترس هستند:
- kotlinx-corountines-core - رابط اصلی برای استفاده از کوروتین ها در Kotlin
- kotlinx-coroutines-android — پشتیبانی از رشته اصلی اندروید در کوروتین ها
برنامه شروع از قبل شامل وابستگی ها در build.gradle.
هنگام ایجاد یک پروژه برنامه جدید، باید build.gradle (Module: app)
را باز کنید و وابستگی های coroutines را به پروژه اضافه کنید.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
در اندروید، اجتناب از مسدود کردن موضوع اصلی ضروری است. رشته اصلی یک رشته است که تمام به روز رسانی های رابط کاربری را مدیریت می کند. همچنین رشتهای است که همه کنترلکنندههای کلیک و سایر تماسهای UI را فراخوانی میکند. به این ترتیب، برای تضمین یک تجربه کاربری عالی، باید به آرامی اجرا شود.
برای اینکه برنامه شما بدون هیچ مکث قابل مشاهده ای به کاربر نمایش داده شود، رشته اصلی باید هر 16 میلی ثانیه یا بیشتر صفحه را به روز کند، که حدود 60 فریم در ثانیه است. بسیاری از کارهای رایج بیشتر از این زمان نیاز دارند، مانند تجزیه مجموعه داده های بزرگ JSON، نوشتن داده ها در پایگاه داده یا واکشی داده ها از شبکه. بنابراین، فراخوانی کدی مانند این از موضوع اصلی میتواند باعث توقف، لکنت یا حتی توقف برنامه شود. و اگر رشته اصلی را برای مدت طولانی مسدود کنید، برنامه حتی ممکن است از کار بیفتد و یک گفتگوی Application Not Responsing ارائه دهد.
ویدیوی زیر را تماشا کنید تا مقدمهای در مورد نحوه حل این مشکل توسط کوروتینها در اندروید با معرفی main-safety را برای ما ببینید.
الگوی تماس برگشتی
یکی از الگوهای انجام وظایف طولانی مدت بدون مسدود کردن رشته اصلی، callbacks است. با استفاده از تماسهای برگشتی، میتوانید کارهای طولانیمدت را روی یک رشته پسزمینه شروع کنید. هنگامی که کار کامل شد، callback فراخوانی می شود تا شما را از نتیجه در موضوع اصلی مطلع کند.
به نمونه ای از الگوی برگشت تماس نگاهی بیندازید.
// 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
اجازه می دهد تا روی یک رشته پس زمینه اجرا شود و پس از آماده شدن نتیجه را برگرداند.
استفاده از کوروتین ها برای حذف تماس ها
Callbacks یک الگوی عالی هستند، با این حال آنها چند اشکال دارند. کدهایی که به شدت از تماسهای برگشتی استفاده میکنند ممکن است خواندن آن سختتر و استدلال کردن در مورد آن سختتر شود. علاوه بر این، تماسهای برگشتی اجازه استفاده از برخی ویژگیهای زبانی مانند استثناها را نمیدهند.
کوروتین های Kotlin به شما امکان می دهند کد مبتنی بر تماس را به کد ترتیبی تبدیل کنید. کدهایی که به صورت متوالی نوشته میشوند معمولاً خواندن آسانتر هستند و حتی میتوانند از ویژگیهای زبانی مانند استثناها استفاده کنند.
در پایان، آنها دقیقاً همان کار را انجام می دهند: صبر کنید تا نتیجه ای از یک کار طولانی مدت در دسترس باشد و به اجرا ادامه دهید. با این حال، در کد آنها بسیار متفاوت به نظر می رسند.
کلمه کلیدی suspend
روشی است که کاتلین برای علامت گذاری یک تابع یا نوع عملکرد در دسترس برای برنامه ها است. هنگامی که برنامهای یک تابع با علامت 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 Studio باز کرده اید.
درک CoroutineScope
در Kotlin، تمام کوروتین ها در یک CoroutineScope
اجرا می شوند. یک scope طول عمر کوروتین ها را از طریق کار خود کنترل می کند. هنگامی که کار یک محدوده را لغو می کنید، تمام کارهای انجام شده در آن محدوده را لغو می کند. در Android، زمانی که کاربر از یک Activity
یا Fragment
دور میشود، میتوانید از یک محدوده برای لغو همه برنامههای در حال اجرا استفاده کنید. Scopes همچنین به شما امکان می دهد یک توزیع کننده پیش فرض را مشخص کنید. یک توزیع کننده کنترل می کند که کدام رشته یک coroutine را اجرا می کند.
برای برنامههایی که توسط UI شروع میشوند، معمولاً درست است که آنها را در Dispatchers.Main
که رشته اصلی Android است شروع کنید. یک برنامه مشترک در Dispatchers.Main
شروع شد. Main رشته اصلی را در حالت تعلیق مسدود نخواهد کرد. از آنجایی که یک برنامه ViewModel
تقریباً همیشه UI را در رشته اصلی به روز می کند، شروع کوروتین ها در رشته اصلی باعث صرفه جویی در سوئیچ های رشته اضافی می شود. برنامهای که در رشته اصلی شروع شده است، میتواند در هر زمانی پس از شروع، توزیعکنندهها را تغییر دهد. به عنوان مثال، می تواند از توزیع کننده دیگری برای تجزیه یک نتیجه بزرگ JSON از رشته اصلی استفاده کند.
با استفاده از viewModelScope
کتابخانه AndroidX lifecycle-viewmodel-ktx
یک CoroutineScope را به ViewModels اضافه می کند که برای شروع کوروتین های مرتبط با رابط کاربری پیکربندی شده است. برای استفاده از این کتابخانه، باید آن را در build.gradle (Module: start)
پروژه خود قرار دهید. این مرحله قبلاً در پروژه های Codelab انجام شده است.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
این کتابخانه یک viewModelScope
را به عنوان تابعی از کلاس ViewModel
اضافه می کند. این محدوده به Dispatchers.Main
محدود شده است و با پاک شدن ViewModel
به طور خودکار لغو می شود.
جابجایی از رشته ها به کوروتین ها
در MainViewModel.kt
TODO بعدی را به همراه این کد پیدا کنید:
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
لغو شود، تمام کارهای روتین در این job/scope لغو خواهند شد. اگر کاربر قبل از بازگشتdelay
، Activity را ترک کند، زمانی کهonCleared
پس از تخریب ViewModel فراخوانی شود، این برنامه به طور خودکار لغو می شود. - از آنجایی که
viewModelScope
یک توزیع کننده پیش فرضDispatchers.Main
دارد، این برنامه در موضوع اصلی راه اندازی می شود. بعداً نحوه استفاده از موضوعات مختلف را خواهیم دید. -
delay
تابع یک تابعsuspend
است. این در اندروید استودیو توسطنماد در ناودان سمت چپ حتی اگر این برنامه روی رشته اصلی اجرا شود،
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")
))
}
}
یک قانون راهی برای اجرای کد قبل و بعد از اجرای یک تست در JUnit است. برای اینکه بتوانیم MainViewModel را در یک تست خارج از دستگاه آزمایش کنیم از دو قانون استفاده می شود:
-
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
، برنامهای که ایجاد کردیم راهاندازی میشود. این آزمایش بررسی میکند که متن ضربهها بلافاصله پس از onMainViewClicked
"0 taps" باقی بماند، سپس 1 ثانیه بعد به "1 taps" بهروزرسانی میشود.
این تست از زمان مجازی برای کنترل اجرای برنامهای که توسط onMainViewClicked
راهاندازی شده است، استفاده میکند. MainCoroutineScopeRule
شما امکان میدهد تا اجرای برنامههایی را که در Dispatchers.Main
راهاندازی میشوند، متوقف کنید، از سر بگیرید یا کنترل کنید. در اینجا ما advanceTimeBy(1_000)
را فراخوانی میکنیم که باعث میشود توزیعکننده اصلی فوراً برنامههایی را که قرار است 1 ثانیه بعد از سر گرفته شوند، اجرا کند.
این تست کاملاً قطعی است، به این معنی که همیشه به همان روش اجرا می شود. و از آنجایی که کنترل کاملی بر اجرای برنامه های برنامه ریزی شده در Dispatchers.Main
دارد، لازم نیست یک ثانیه منتظر بمانید تا مقدار تنظیم شود.
تست موجود را اجرا کنید
- روی نام کلاس
MainViewModelTest
در ویرایشگر خود کلیک راست کنید تا منوی زمینه باز شود. - در منوی زمینه را انتخاب کنید
"MainViewModelTest" را اجرا کنید
- برای اجراهای بعدی می توانید این پیکربندی آزمایشی را در تنظیمات کناری انتخاب کنید
دکمه در نوار ابزار به طور پیش فرض، پیکربندی MainViewModelTest نامیده می شود.
شما باید قبولی آزمون را ببینید! و اجرای آن باید کمی کمتر از یک ثانیه طول بکشد.
در تمرین بعدی یاد خواهید گرفت که چگونه از یک APIهای پاسخ به تماس موجود به استفاده از کوروتین ها تبدیل کنید.
در این مرحله، تبدیل یک مخزن برای استفاده از کوروتین ها را شروع می کنید. برای انجام این کار، ما کوروتین ها را به ViewModel
، Repository
، Room
و Retrofit
اضافه می کنیم.
این ایده خوبی است که قبل از اینکه آنها را به استفاده از کوروتین ها تغییر دهیم، درک کنیم که هر بخش از معماری چه مسئولیتی دارد.
-
MainDatabase
یک پایگاه داده را با استفاده از Room پیاده سازی می کند که یکTitle
را ذخیره و بارگذاری می کند. -
MainNetwork
یک API شبکه را پیاده سازی می کند که یک عنوان جدید واکشی می کند. از Retrofit برای واکشی عناوین استفاده می کند.Retrofit
طوری پیکربندی شده است که بهطور تصادفی خطاها یا دادههای ساختگی را برمیگرداند، اما در غیر این صورت طوری رفتار میکند که گویی درخواستهای شبکه واقعی را ارائه میکند. -
TitleRepository
یک API واحد را برای واکشی یا بهروزرسانی عنوان با ترکیب دادهها از شبکه و پایگاه داده پیادهسازی میکند. -
MainViewModel
وضعیت صفحه نمایش را نشان می دهد و رویدادها را مدیریت می کند. وقتی کاربر روی صفحه ضربه میزند، به مخزن میگوید که عنوان را تازهسازی کند.
از آنجایی که درخواست شبکه توسط رویدادهای UI هدایت میشود و میخواهیم بر اساس آنها یک برنامه مشترک شروع کنیم، مکان طبیعی برای شروع استفاده از کوروتینها در ViewModel
است.
نسخه برگشت به تماس
برای مشاهده اعلان refreshTitle
، MainViewModel.kt
را باز کنید.
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)
}
})
}
این تابع هر بار که کاربر روی صفحه کلیک می کند فراخوانی می شود - و باعث می شود که مخزن عنوان را تازه کرده و عنوان جدید را در پایگاه داده بنویسد.
این پیاده سازی از یک callback برای انجام چند کار استفاده می کند:
- قبل از شروع یک پرس و جو، یک اسپینر بارگیری را با
_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)
}
هنگامی که کار با این کد لبه تمام شد، آن را به روز می کنید تا از Retrofit و Room برای واکشی عنوان جدید و نوشتن آن در پایگاه داده با استفاده از کوروتین ها استفاده کنید. در حال حاضر، فقط 500 میلی ثانیه را صرف تظاهر به انجام کار می کند و سپس ادامه می دهد.
در MainViewModel
، نسخه callback 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
جدید در 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
}
استثناها در توابع تعلیق درست مانند خطاهای توابع معمولی عمل می کنند. اگر در یک تابع تعلیق خطا ایجاد کنید، به تماس گیرنده ارسال می شود. بنابراین حتی اگر آنها کاملاً متفاوت اجرا می شوند، می توانید از بلوک های معمولی try/catch برای مدیریت آنها استفاده کنید. این مفید است زیرا به شما امکان می دهد به جای ایجاد مدیریت سفارشی خطا برای هر پاسخ به تماس، به پشتیبانی زبان داخلی برای مدیریت خطا تکیه کنید.
و، اگر استثنایی را از یک کوروتین خارج کنید - آن کوروتین به طور پیشفرض، والد آن را لغو میکند. این بدان معناست که لغو چندین کار مرتبط با هم آسان است.
و سپس، در یک بلوک نهایی، میتوانیم مطمئن شویم که spinner همیشه پس از اجرای پرسوجو خاموش است.
با انتخاب تنظیمات شروع و سپس فشار دادن مجدد برنامه را اجرا کنید ، وقتی روی هر جایی ضربه می زنید، باید یک اسپینر در حال بارگذاری را ببینید. عنوان ثابت خواهد ماند زیرا ما هنوز شبکه یا پایگاه داده خود را متصل نکرده ایم.
در تمرین بعدی، مخزن را بهروزرسانی میکنید تا واقعاً کار انجام دهد.
در این تمرین میآموزید که چگونه رشتهای را که یک کوروتین روی آن اجرا میشود تغییر دهید تا یک نسخه کارآمد از TitleRepository
را پیادهسازی کنید.
کد برگشت تماس موجود را در refreshTitle مرور کنید
TitleRepository.kt
را باز کنید و پیاده سازی مبتنی بر callback موجود را بررسی کنید.
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
با یک callback برای ارتباط وضعیت بارگیری و خطا به تماس گیرنده پیاده سازی می شود.
این تابع چند کار را برای اجرای تازه سازی انجام می دهد.
- با
BACKGROUND
ExecutorService
به رشته دیگری بروید - درخواست شبکه
fetchNextTitle
را با استفاده از روش blockingexecute()
. این درخواست شبکه را در رشته فعلی اجرا میکند، در این مورد یکی از رشتههای موجود درBACKGROUND
. - اگر نتیجه موفقیت آمیز بود، آن را با
insertTitle
در پایگاه داده ذخیره کنید وonCompleted()
را فراخوانی کنید. - اگر نتیجه موفقیت آمیز نبود، یا استثنا وجود داشت، با متد onError تماس بگیرید تا به تماس گیرنده در مورد رفرش ناموفق بگویید.
این پیادهسازی مبتنی بر تماس، ایمن اصلی است زیرا رشته اصلی را مسدود نمیکند. اما، باید از یک تماس برگشتی استفاده کند تا پس از اتمام کار به تماس گیرنده اطلاع دهد. همچنین تماسهای برگشتی را در رشته BACKGROUND
که آن را نیز تغییر داده است فراخوانی میکند.
مسدود کردن تماسها از کوروتینها
بدون معرفی کوروتینها به شبکه یا پایگاه داده، میتوانیم این کد را با استفاده از کوروتینها ایمن اصلی کنیم. این به ما اجازه میدهد تا از شر callback خلاص شویم و نتیجه را به رشتهای که ابتدا آن را فراخوانی کرده بود برگردانیم.
میتوانید از این الگو در هر زمانی که نیاز به انجام کارهای مسدود کردن یا فشردهسازی CPU از داخل یک برنامه معمولی مانند مرتبسازی و فیلتر کردن یک لیست بزرگ یا خواندن از روی دیسک دارید، استفاده کنید.
برای جابهجایی بین هر توزیعکننده، coroutines از withContext
استفاده میکند. فراخوانی withContext
به توزیع کننده دیگر فقط برای لامبدا تغییر می کند و سپس با نتیجه آن لامبدا به توزیع کننده ای که آن را فراخوانی کرده است باز می گردد.
به طور پیش فرض، Kotlin coroutines سه Dispatcher را ارائه می دهد: Main
، IO
و Default
. توزیع کننده IO برای کارهای 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(...)
هر دو رشتهای را که این coroutine در آن اجرا میشود مسدود میکند. با این حال، با جابجایی به Dispatchers.IO
با استفاده از withContext
، یکی از رشتههای موجود در توزیع کننده IO را مسدود میکنیم. برنامهای که این را نامیده است، احتمالاً در Dispatchers.Main
اجرا میشود، تا زمانی که withContext
lambda کامل شود، به حالت تعلیق در میآید.
در مقایسه با نسخه برگشت، دو تفاوت مهم وجود دارد:
-
withContext
نتیجه آن را به Dispatcher که آن را فراخوانی کرده است برمیگرداند، در این موردDispatchers.Main
. نسخه برگشت به تماس تماسهای موجود در یک رشته در سرویس اجراکنندهBACKGROUND
نامیده میشود. - تماس گیرنده نیازی به ارسال یک تماس به این تابع ندارد. آنها می توانند برای دریافت نتیجه یا خطا به تعلیق و از سرگیری اعتماد کنند.
دوباره برنامه را اجرا کنید
اگر دوباره برنامه را اجرا کنید، خواهید دید که اجرای جدید مبتنی بر کوروتین ها نتایج را از شبکه بارگیری می کند!
در مرحله بعدی، کوروتین ها را در Room و Retrofit ادغام خواهید کرد.
برای ادامه یکپارچهسازی کوروتینها، میخواهیم از پشتیبانی از توابع تعلیق در نسخه پایدار Room و Retrofit استفاده کنیم، سپس کدی را که نوشتیم تا حد زیادی با استفاده از توابع تعلیق سادهسازی کنیم.
کوروتین ها در اتاق
ابتدا MainDatabase.kt
را باز کنید و insertTitle
را یک تابع suspend کنید:
پایگاه داده اصلی.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
وقتی این کار را انجام میدهید، Room پرس و جوی شما را ایمن میکند و آن را به صورت خودکار روی یک رشته پسزمینه اجرا میکند. با این حال، به این معنی است که شما فقط می توانید این پرس و جو را از داخل یک کوروتین فراخوانی کنید.
و - این تمام کاری است که برای استفاده از کوروتین ها در اتاق باید انجام دهید. خیلی باحاله
Coroutines در Retrofit
در ادامه بیایید نحوه ادغام کوروتین ها با Retrofit را ببینیم. MainNetwork.kt
را باز کنید و fetchNextTitle
را به یک تابع suspend تغییر دهید.
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
wrapper را از نوع برگشتی جدا کنید. در اینجا ماString
را برمی گردانیم، اما می توانید نوع پیچیده با پشتوانه json را نیز برگردانید. اگر همچنان میخواهید دسترسی بهResult
کامل retrofit را فراهم کنید، میتوانیدResult<String>
را به جایString
از تابع suspend برگردانید.
Retrofit به طور خودکار عملکردهای تعلیق را ایمن اصلی می کند تا بتوانید مستقیماً از Dispatchers.Main
با آنها تماس بگیرید.
استفاده از Room و Retrofit
اکنون که Room و 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
جعلیهای آزمایشی را برای پشتیبانی از اصلاحکنندههای تعلیق جدید بهروزرسانی کنید.
عنوانDaoFake
- کلید alt-enter را بزنید تا اصلاح کننده های تعلیق را به همه توابع موجود در هیرانشی اضافه کنید
شبکه اصلی فیک
- کلید alt-enter را بزنید تا اصلاح کننده های تعلیق را به همه توابع موجود در هیرانشی اضافه کنید
- این تابع را جایگزین
fetchNextTitle
کنید
override suspend fun fetchNextTitle() = result
شبکه اصلی CompletableFake
- کلید alt-enter را بزنید تا اصلاح کننده های تعلیق را به همه توابع موجود در هیرانشی اضافه کنید
- این تابع را جایگزین
fetchNextTitle
کنید
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- تابع
refreshTitleWithCallbacks
را حذف کنید زیرا دیگر استفاده نمی شود.
برنامه را اجرا کنید
برنامه را دوباره اجرا کنید، پس از کامپایل، خواهید دید که در حال بارگیری داده ها با استفاده از کوروتین ها از ViewModel تا Room و Retrofit است!
تبریک میگوییم، شما این برنامه را به طور کامل با استفاده از برنامههای آموزشی جایگزین کردهاید! برای جمع بندی، کمی در مورد نحوه آزمایش کاری که اخیرا انجام دادیم صحبت خواهیم کرد.
در این تمرین، آزمایشی می نویسید که مستقیماً یک تابع suspend
فراخوانی می کند.
Since refreshTitle
is exposed as a public API it will be tested directly, showing how to call coroutines functions from tests.
Here's the refreshTitle
function you implemented in the last exercise:
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)
}
}
Write a test that calls a suspend function
Open TitleRepositoryTest.kt
in the test
folder which has two TODOS.
Try to call refreshTitle
from the first test whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Since refreshTitle
is a suspend
function Kotlin doesn't know how to call it except from a coroutine or another suspend function, and you will get a compiler error like, "Suspend function refreshTitle should be called only from a coroutine or another suspend function."
The test runner doesn't know anything about coroutines so we can't make this test a suspend function. We could launch
a coroutine using a CoroutineScope
like in a ViewModel
, however tests need to run coroutines to completion before they return. Once a test function returns, the test is over. Coroutines started with launch
are asynchronous code, which may complete at some point in the future. Therefore to test that asynchronous code, you need some way to tell the test to wait until your coroutine completes. Since launch
is a non-blocking call, that means it returns right away and can continue to run a coroutine after the function returns - it can't be used in tests. مثلا:
@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
}
This test will sometimes fail. The call to launch
will return immediately and execute at the same time as the rest of the test case. The test has no way to know if refreshTitle
has run yet or not – and any assertions like checking that the database was updated would be flakey. And, if refreshTitle
threw an exception, it will not be thrown in the test call stack. It will instead be thrown into GlobalScope
's uncaught exception handler.
The library kotlinx-coroutines-test
has the runBlockingTest
function that blocks while it calls suspend functions. When runBlockingTest
calls a suspend function or launches
a new coroutine, it executes it immediately by default. You can think of it as a way to convert suspend functions and coroutines into normal function calls.
In addition, runBlockingTest
will rethrow uncaught exceptions for you. This makes it easier to test when a coroutine is throwing an exception.
Implement a test with one coroutine
Wrap the call to refreshTitle
with runBlockingTest
and remove the GlobalScope.launch
wrapper from 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")
}
This test uses the fakes provided to check that "OK" is inserted to the database by refreshTitle
.
When the test calls runBlockingTest
, it will block until the coroutine started by runBlockingTest
completes. Then inside, when we call refreshTitle
it uses the regular suspend and resume mechanism to wait for the database row to be added to our fake.
After the test coroutine completes, runBlockingTest
returns.
Write a timeout test
We want to add a short timeout to the network request. Let's write the test first then implement the timeout. Create a new test:
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)
}
This test uses the provided fake MainNetworkCompletableFake
, which is a network fake that's designed to suspend callers until the test continues them. When refreshTitle
tries to make a network request, it'll hang forever because we want to test timeouts.
Then, it launches a separate coroutine to call refreshTitle
. This is a key part of testing timeouts, the timeout should happen in a different coroutine than the one runBlockingTest
creates. By doing so, we can call the next line, advanceTimeBy(5_000)
which will advance time by 5 seconds and cause the other coroutine to timeout.
This is a complete timeout test, and it will pass once we implement timeout.
Run it now and see what happens:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
One of the features of runBlockingTest
is that it won't let you leak coroutines after the test completes. If there are any unfinished coroutines, like our launch coroutine, at the end of the test, it will fail the test.
Add a timeout
Open up TitleRepository
and add a five second timeout to the network fetch. You can do this by using the withTimeout
function:
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)
}
}
Run the test. When you run the tests you should see all tests pass!
In the next exercise you'll learn how to write higher order functions using coroutines.
In this exercise you'll refactor refreshTitle
in MainViewModel
to use a general data loading function. This will teach you how to build higher order functions that use coroutines.
The current implementation of refreshTitle
works, but we can create a general data loading coroutine that always shows the spinner. This might be helpful in a codebase that loads data in response to several events, and wants to ensure the loading spinner is consistently displayed.
Reviewing the current implementation every line except repository.refreshTitle()
is boilerplate to show the spinner and display errors.
// 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
}
}
}
Using coroutines in higher order functions
Add this code to 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
}
}
}
Now refactor refreshTitle()
to use this higher order function.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
By abstracting the logic around showing a loading spinner and showing errors, we've simplified our actual code needed to load data. Showing a spinner or displaying an error is something that's easy to generalize to any data loading, while the actual data source and destination needs to be specified every time.
To build this abstraction, launchDataLoad
takes an argument block
that is a suspend lambda. A suspend lambda allows you to call suspend functions. That's how Kotlin implements the coroutine builders launch
and runBlocking
we've been using in this codelab.
// suspend lambda
block: suspend () -> Unit
To make a suspend lambda, start with the suspend
keyword. The function arrow and return type Unit
complete the declaration.
You don't often have to declare your own suspend lambdas, but they can be helpful to create abstractions like this that encapsulate repeated logic!
In this exercise you'll learn how to use coroutine based code from WorkManager.
What is WorkManager
There are many options on Android for deferrable background work. This exercise shows you how to integrate WorkManager with coroutines. WorkManager is a compatible, flexible and simple library for deferrable background work. WorkManager is the recommended solution for these use cases on Android.
WorkManager is part of Android Jetpack , and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app.
Because of this, WorkManager is a good choice for tasks that must complete eventually.
Some examples of tasks that are a good use of WorkManager:
- Uploading logs
- Applying filters to images and saving the image
- Periodically syncing local data with the network
Using coroutines with WorkManager
WorkManager provides different implementations of its base ListanableWorker
class for different use cases.
The simplest Worker class allows us to have some synchronous operation executed by WorkManager. However, having worked so far to convert our codebase to use coroutines and suspend functions, the best way to use WorkManager is through the CoroutineWorker
class that allows to define our doWork()
function as a suspend function.
To get started, open up RefreshMainDataWork
. It already extends CoroutineWorker
, and you need to implement doWork
.
Inside the suspend
doWork
function, call refreshTitle()
from the repository and return the appropriate result!
After you've completed the TODO, the code will look like this:
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()
}
}
Note that CoroutineWorker.doWork()
is a suspending function. Unlike the simpler Worker
class, this code does NOT run on the Executor specified in your WorkManager configuration, but instead use the dispatcher in coroutineContext
member (by default Dispatchers.Default
).
Testing our CoroutineWorker
No codebase should be complete without testing.
WorkManager makes available a couple of different ways to test your Worker
classes, to learn more about the original testing infrastructure, you can read the documentation .
WorkManager v2.1 introduces a new set of APIs to support a simpler way to test ListenableWorker
classes and, as a consequence, CoroutineWorker. In our code we're going to use one of these new API: TestListenableWorkerBuilder
.
To add our new test, update the RefreshMainDataWorkTest
file under the androidTest
folder.
The content of the file is:
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())
}
}
Before we get to the test, we tell WorkManager
about the factory so we can inject the fake network.
The test itself uses the TestListenableWorkerBuilder
to create our worker that we can then run calling the startWork()
method.
WorkManager is just one example of how coroutines can be used to simplify APIs design.
In this codelab we have covered the basics you'll need to start using coroutines in your app!
We covered:
- How to integrate coroutines to Android apps from both the UI and WorkManager jobs to simplify asynchronous programming,
- How to use coroutines inside a
ViewModel
to fetch data from the network and save it to a database without blocking the main thread. - And how to cancel all coroutines when the
ViewModel
is finished.
For testing coroutine based code, we covered both by testing behavior as well as directly calling suspend
functions from tests.
Learn more
Check out the " Advanced Coroutines with Kotlin Flow and LiveData " codelab to learn more advanced coroutines usage on Android.
Kotlin coroutines have many features that weren't covered by this codelab. If you're interested in learning more about Kotlin coroutines, read the coroutines guides published by JetBrains. Also check out " Improve app performance with Kotlin coroutines " for more usage patterns of coroutines on Android.