در این لبه کد یاد خواهید گرفت که چگونه از 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های موجود به کوروتین بیاموزید. - از کوروتین ها با کامپوننت های معماری استفاده کنید.
- بهترین روش ها را برای آزمایش کوروتین ها بیاموزید.
آنچه شما نیاز دارید
- Android Studio 3. 5 (ممکن است نرم افزار کد با نسخه های دیگر کار کند، اما ممکن است برخی چیزها گم شده باشند یا متفاوت به نظر برسند).
اگر در حین کار با این کد با مشکلاتی (اشکالات کد، خطاهای دستوری، عبارت نامشخص و غیره) مواجه شدید، لطفاً مشکل را از طریق پیوند گزارش یک اشتباه در گوشه سمت چپ پایین صفحه کد گزارش کنید.
کد را دانلود کنید
برای دانلود تمامی کدهای این کد لبه روی لینک زیر کلیک کنید:
... یا با استفاده از دستور زیر مخزن GitHub را از خط فرمان کلون کنید:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
سوالات متداول
ابتدا، بیایید ببینیم که برنامه نمونه شروع به چه شکلی است. برای باز کردن نمونه برنامه در Android Studio این دستورالعمل ها را دنبال کنید.
- اگر فایل فشرده
kotlin-coroutines
را دانلود کردید، فایل را از حالت فشرده خارج کنید. - پروژه
coroutines-codelab
را در Android Studio باز کنید. - ماژول برنامه
start
انتخاب کنید. - را کلیک کنید
دکمه اجرا ، و یا یک شبیه ساز انتخاب کنید یا دستگاه Android خود را که باید قابلیت اجرای Android Lollipop را داشته باشد وصل کنید (حداقل SDK پشتیبانی شده 21 است). صفحه Kotlin Coroutines باید ظاهر شود:
این برنامه شروع کننده از رشته ها برای افزایش شمارش با تاخیری کوتاه پس از فشار دادن صفحه استفاده می کند. همچنین عنوان جدیدی را از شبکه دریافت می کند و آن را روی صفحه نمایش می دهد. اکنون آن را امتحان کنید و پس از یک تاخیر کوتاه، تعداد و پیام تغییر می کند. در این کد لبه شما این برنامه را به استفاده از کوروتین ها تبدیل می کنید.
این برنامه از اجزای معماری برای جدا کردن کد رابط کاربری در MainActivity
از منطق برنامه در MainViewModel
استفاده می کند. یک لحظه برای آشنایی با ساختار پروژه وقت بگذارید.
-
MainActivity
رابط کاربری را نمایش می دهد، شنوندگان کلیک را ثبت می کند و می تواند یکSnackbar
را نمایش دهد. رویدادها را بهMainViewModel
منتقل می کند و صفحه را بر اساسLiveData
درMainViewModel
به روز می کند. -
MainViewModel
رویدادها را درonMainViewClicked
مدیریت می کند و با استفاده از LiveData باMainActivity
ارتباط برقرار می کندLiveData.
-
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 { ... }
در بخش بعدی برنامه نمونه کار را با کوروتین ها معرفی خواهید کرد.
در این تمرین شما یک کوروتین می نویسید تا بعد از تاخیر یک پیام نمایش داده شود. برای شروع، مطمئن شوید که ماژول را در Android Studio باز start
اید.
درک CoroutineScope
در Kotlin، همه کوروتین ها در یک CoroutineScope
اجرا می شوند. یک دامنه از طریق کار خود طول عمر کوروتین ها را کنترل می کند. هنگامی که کار یک محدوده را لغو می کنید، تمام کارهای انجام شده در آن محدوده را لغو می کند. در Android، زمانی که کاربر از یک Activity
یا Fragment
دور میشود، میتوانید از یک محدوده برای لغو همه برنامههای در حال اجرا استفاده کنید. Scopes همچنین به شما امکان می دهد یک توزیع کننده پیش فرض را مشخص کنید. یک توزیع کننده کنترل می کند که کدام رشته یک coroutine را اجرا می کند.
برای برنامههایی که توسط UI شروع میشوند، معمولاً درست است که آنها را در Dispatchers.Main
که رشته اصلی Android است شروع کنید. یک برنامه مشترک در Dispatchers.Main
شروع شد. Main تا زمانی که به حالت تعلیق درآمده است، رشته اصلی را مسدود نخواهد کرد. از آنجایی که یک برنامه ViewModel
تقریبا همیشه رابط کاربری را در رشته اصلی بهروزرسانی میکند، شروع کوروتینها در رشته اصلی باعث صرفهجویی در سوئیچهای رشته اضافی میشود. برنامهای که در رشته اصلی شروع شده است، میتواند در هر زمانی پس از شروع، توزیعکنندهها را تغییر دهد. به عنوان مثال، می تواند از توزیع کننده دیگری برای تجزیه یک نتیجه بزرگ JSON از رشته اصلی استفاده کند.
با استفاده از viewModelScope
کتابخانه AndroidX lifecycle-viewmodel-ktx
یک CoroutineScope را به ViewModels اضافه میکند که برای شروع کوروتینهای مرتبط با UI پیکربندی شده است. برای استفاده از این کتابخانه، باید آن را در فایل 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
فعالیت را ترک کند، زمانی که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
است.
نسخه پاسخ به تماس
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)
}
})
}
این تابع هر بار که کاربر روی صفحه کلیک می کند فراخوانی می شود - و باعث می شود که مخزن عنوان را تازه کرده و عنوان جدید را در پایگاه داده بنویسد.
این پیاده سازی از یک 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
شروع کنید. این از 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()
فراخوانی کنید. - اگر نتیجه موفقیت آمیز نبود یا استثنا وجود داشت، با متد oneError تماس بگیرید تا به تماس گیرنده در مورد رفرش ناموفق بگویید.
این پیادهسازی مبتنی بر پاسخ به تماس، ایمن اصلی است زیرا رشته اصلی را مسدود نمیکند. اما، باید از تماس برگشتی استفاده کند تا پس از اتمام کار به تماس گیرنده اطلاع دهد. همچنین تماسهای برگشتی در رشته BACKGROUND
که آن را نیز تغییر داده است فراخوانی میکند.
مسدود کردن تماسها از کوروتینها
بدون معرفی کوروتینها به شبکه یا پایگاه داده، میتوانیم این کد را با استفاده از کوروتینها ایمن اصلی کنیم. این به ما امکان میدهد از کالبک خلاص شویم و نتیجه را به رشتهای که ابتدا آن را فراخوانی کرده بود برگردانیم.
میتوانید از این الگو در هر زمانی که نیاز به انجام کارهای مسدود کردن یا فشردهسازی 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 dispatcher را مسدود میکنیم. برنامهای که این را نامیده است، احتمالاً در Dispatchers.Main
اجرا میشود، تا زمانی که withContext
lambda کامل شود، به حالت تعلیق در میآید.
در مقایسه با نسخه برگشت، دو تفاوت مهم وجود دارد:
-
withContext
نتیجه را به Dispatcher که آن را فراخوانی کرده است برمیگرداند، در این موردDispatchers.Main
. نسخه ی callback callback های موجود در یک رشته در سرویس اجرا کننده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 اضافه کردن تعلیق تعلیق به همه توابع در heiranchy
شبکه اصلی فیک
- دکمه alt-enter اضافه کردن تعلیق تعلیق به همه توابع در heiranchy
- این تابع را جایگزین
fetchNextTitle
کنید
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- دکمه alt-enter اضافه کردن تعلیق تعلیق به همه توابع در heiranchy
- این تابع را جایگزین
fetchNextTitle
کنید
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- تابع
refreshTitleWithCallbacks
را حذف کنید زیرا دیگر استفاده نمی شود.
برنامه را اجرا کنید
برنامه را دوباره اجرا کنید، پس از کامپایل، خواهید دید که در حال بارگیری داده ها با استفاده از کوروتین ها از ViewModel تا Room و Retrofit است!
تبریک میگوییم، شما این برنامه را به طور کامل با استفاده از کوروتینها عوض کردهاید! برای بسته بندی ما کمی در مورد چگونگی آزمایش آنچه که ما انجام دادیم صحبت خواهیم کرد.
در این تمرین ، تست هایی را می نویسید که مستقیماً عملکرد suspend
را صدا می کند.
از آنجا که refreshTitle
به عنوان یک API عمومی در معرض دید قرار می گیرد ، مستقیماً مورد آزمایش قرار می گیرد و نشان می دهد که چگونه می توان توابع Coroutines را از آزمایشات فراخوانی کرد.
در اینجا عملکرد 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
که دارای دو TODOS است ، باز کنید.
سعی کنید از اولین تست whenRefreshTitleSuccess_insertsRows
refreshTitle
استفاده کنید.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
از آنجا که refreshTitle
یک تابع suspend
است که Kotlin نمی داند چگونه آن را صدا کند ، مگر از یک Coroutine یا یک عملکرد معلق دیگر ، و شما یک خطای کامپایلر مانند ، "عملکرد تعلیق RefreshTitle را باید فقط از یک Coroutine یا یک عملکرد معلق دیگر نامید."
دونده تست چیزی در مورد Coroutines نمی داند ، بنابراین ما نمی توانیم این آزمایش را به یک عملکرد معلق تبدیل کنیم. ما می توانیم یک Coroutine را با استفاده از یک CoroutineScope
مانند یک ViewModel
launch
، اما آزمایشات لازم است قبل از بازگشت ، Coroutines را اجرا کنند. پس از بازگشت یک عملکرد تست ، آزمون به پایان رسید. Coroutines با launch
کد ناهمزمان است که ممکن است در برخی از نقاط آینده تکمیل شود. بنابراین برای آزمایش آن کد ناهمزمان ، به راهی نیاز دارید تا به آزمایش بگویید تا صبر کنید تا Coroutine شما تمام شود. از آنجا که launch
یک تماس غیر مسدود کننده است ، این بدان معنی است که بلافاصله باز می گردد و می تواند پس از بازده عملکرد ، به اجرای Coroutine ادامه دهد - در آزمایشات قابل استفاده نیست. به عنوان مثال:
@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
پرتاب می شود.
kotlinx-coroutines-test
دارای عملکرد runBlockingTest
است که در حالی که توابع تعلیق را صدا می کند مسدود می شود. هنگامی که runBlockingTest
یک عملکرد تعلیق را فراخوانی می کند یا یک Coroutine جدید launches
، بلافاصله آن را به طور پیش فرض اجرا می کند. شما می توانید از آن به عنوان راهی برای تبدیل توابع تعلیق و Coroutines به تماس های عملکرد عادی فکر کنید.
علاوه بر این ، runBlockingTest
استثنائات Uncaughip را برای شما تجدید نظر می کند. این امر باعث می شود آزمایش در هنگام پرتاب یک استثناء آسان تر شود.
یک آزمایش را با یک Coroutine اجرا کنید
تماس را برای refreshTitle
با runBlockingTest
بسته بندی کنید و GlobalScope.launch
حذف کنید. بسته بندی Launch از موضوع. 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" توسط refreshTitle
به پایگاه داده درج شده است ، استفاده می کند.
هنگامی که تماس های آزمایشی runBlockingTest
، مسدود می شود تا زمانی که Coroutine با شروع runBlockingTest
شروع شود. سپس در داخل ، هنگامی که ما با refreshTitle
تماس می گیریم ، از مکانیزم معلق و رزومه منظم استفاده می کند تا منتظر بمانید تا ردیف پایگاه داده به جعلی ما اضافه شود.
پس از اتمام تست Coroutine ، runBlockingTest
بازگشت.
یک آزمون Timeout بنویسید
ما می خواهیم یک زمان کوتاه به درخواست شبکه اضافه کنیم. بیایید ابتدا آزمون را بنویسیم و سپس زمان را پیاده سازی کنیم. یک تست جدید ایجاد کنید:
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
سعی می کند یک درخواست شبکه ایجاد کند ، برای همیشه آویزان خواهد شد زیرا ما می خواهیم زمان های آزمایش را آزمایش کنیم.
سپس ، یک Coroutine جداگانه را برای تماس با refreshTitle
راه اندازی می کند. این بخش مهمی از تست های آزمایشی است ، زمان وقفه باید در یک Coroutine متفاوت از آنچه runBlockingTest
ایجاد می کند اتفاق بیفتد. با این کار ، می توانیم با خط بعدی ، advanceTimeBy(5_000)
تماس بگیریم که 5 ثانیه از آن زمان می برد و باعث می شود تا زمان دیگر Coroutine به پایان برسد.
این یک تست کامل زمان است و پس از اجرای Timeout ، آن را پشت سر می گذارد.
اکنون آن را اجرا کنید و ببینید چه اتفاقی می افتد:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
یکی از ویژگی های runBlockingTest
این است که پس از اتمام آزمایش ، به شما اجازه نمی دهد Coroutines را نشت کنید. اگر Coroutines ناتمام ، مانند Coroutine پرتاب ما ، در پایان آزمایش وجود داشته باشد ، این آزمایش را شکست می دهد.
یک زمان بندی اضافه کنید
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)
}
}
تست را اجرا کنید. وقتی تست ها را اجرا می کنید ، باید همه تست ها را پشت سر بگذارید!
در تمرین بعدی شما یاد می گیرید که چگونه با استفاده از Coroutines توابع مرتبه بالاتر را بنویسید.
در این تمرین ، Refactor refreshTitle
در MainViewModel
برای استفاده از یک عملکرد بارگیری داده کلی استفاده خواهید کرد. این به شما می آموزد که چگونه کارکردهای مرتبه بالاتری را که از Coroutines استفاده می کنند ، بسازید.
اجرای فعلی refreshTitle
کار می کند ، اما ما می توانیم یک Coroutine بارگیری داده های کلی ایجاد کنیم که همیشه اسپینر را نشان می دهد. این ممکن است در یک پایگاه کد که داده ها را در پاسخ به چندین رویداد بارگیری می کند ، مفید باشد و می خواهد اطمینان حاصل کند که اسپینر بارگیری به طور مداوم نمایش داده می شود.
بررسی اجرای فعلی هر خط به جز 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
}
}
}
استفاده از Coroutines در توابع مرتبه بالاتر
این کد را به 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
}
}
}
اکنون Refactor refreshTitle()
برای استفاده از این عملکرد مرتبه بالاتر.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
با انتزاع منطق پیرامون نشان دادن یک اسپینر بارگیری و نشان دادن خطاها ، ما کد واقعی مورد نیاز خود را برای بارگیری داده ها ساده کرده ایم. نمایش اسپینر یا نشان دادن خطا چیزی است که به راحتی می توان به هرگونه بارگیری داده تعمیم داد ، در حالی که منبع داده و مقصد واقعی هر بار باید مشخص شود.
برای ساختن این انتزاع ، launchDataLoad
یک block
می گیرد که یک لامبدا معلق است. یک لامبدا معلق به شما امکان می دهد توابع تعلیق را صدا کنید. اینگونه است که کوتلین از launch
و اجرای ساختگیرهای Coroutine که ما در این CodeLab استفاده کرده ایم ، runBlocking
.
// suspend lambda
block: suspend () -> Unit
برای ایجاد یک لامبدا معلق ، با کلمه کلیدی suspend
شروع کنید. واحد پیکان و Unit
بازگردانی اعلامیه را تکمیل می کند.
شما اغلب لازم نیست که لامبدهای معلق خود را اعلام کنید ، اما آنها می توانند برای ایجاد انتزاعی مانند این که منطق مکرر را محاصره می کنند ، مفید باشند!
در این تمرین یاد می گیرید که چگونه از کد مبتنی بر Coroutine از WorkManager استفاده کنید.
کارگر چیست
گزینه های زیادی در Android برای کارهای پس زمینه قابل تعویض وجود دارد. این تمرین به شما نشان می دهد که چگونه می توانید کارگر را با Coroutines ادغام کنید. WorkManager یک کتابخانه سازگار ، انعطاف پذیر و ساده برای کارهای پس زمینه قابل تعویض است. WorkManager راه حل پیشنهادی برای این موارد استفاده در Android است.
WorkManager بخشی از Android Jetpack و یک مؤلفه معماری برای کارهای پس زمینه است که به ترکیبی از اجرای فرصت طلب و تضمین شده نیاز دارد. اعدام فرصت طلب به این معنی است که WorkManager کار پیش زمینه شما را در اسرع وقت انجام می دهد. اجرای تضمین شده به این معنی است که WorkManager برای شروع کار خود در شرایط مختلف ، از منطق مراقبت خواهد کرد ، حتی اگر از برنامه خود دور شوید.
به همین دلیل ، WorkManager انتخاب خوبی برای کارهایی است که باید در نهایت انجام شود.
برخی از نمونه هایی از کارهایی که استفاده خوبی از کارگر است:
- آپلودهای بارگذاری
- استفاده از فیلترها در تصاویر و ذخیره تصویر
- همگام سازی دوره های محلی با شبکه
استفاده از Coroutines با کارگر
WorkManager پیاده سازی های مختلفی از کلاس پایه ListanableWorker
خود را برای موارد مختلف استفاده ارائه می دهد.
ساده ترین کلاس کارگر به ما این امکان را می دهد تا برخی از عملیات همزمان را که توسط WorkManager اجرا شده است ، انجام دهیم. با این حال ، با کار تاکنون برای تبدیل پایگاه کد ما برای استفاده از Coroutines و تعلیق توابع ، بهترین راه برای استفاده از WorkManager از طریق کلاس CoroutineWorker
است که اجازه می دهد عملکرد doWork()
ما را به عنوان یک عملکرد تعلیق تعریف کنیم.
برای شروع ، RefreshMainDataWork
باز کنید. این در حال حاضر CoroutineWorker
را گسترش می دهد ، و شما باید doWork
پیاده سازی کنید.
در داخل عملکرد doWork
suspend
، با refreshTitle()
از مخزن تماس بگیرید و نتیجه مناسب را برگردانید!
پس از اتمام TODO ، کد به این شکل خواهد بود:
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 شما اجرا نمی شود ، بلکه در عوض از Dispatcher در عضو coroutineContext
استفاده می کند (به طور پیش فرض Dispatchers.Default
).
آزمایش Coroutineworker ما
هیچ پایه کد نباید بدون آزمایش کامل باشد.
WorkManager چند روش مختلف برای آزمایش کلاس های Worker
خود ، برای کسب اطلاعات بیشتر در مورد زیرساخت های آزمایش اصلی ، می توانید مستندات را بخوانید.
WorkManager v2.1 مجموعه جدیدی از API ها را برای پشتیبانی از یک روش ساده تر برای آزمایش کلاس های ListenableWorker
و به عنوان یک نتیجه ، CoroutineWorker معرفی می کند. در کد ما می خواهیم از یکی از این API جدید استفاده کنیم: 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 فقط یک نمونه از نحوه استفاده از Coroutines برای ساده سازی طراحی APIS است.
در این CodeLab ما اصول اولیه را پوشش داده ایم که باید از Coroutines در برنامه خود استفاده کنید!
پوشش دادیم:
- نحوه ادغام Coroutines در برنامه های Android از هر دو کار UI و WorkManager برای ساده سازی برنامه نویسی ناهمزمان ،
- نحوه استفاده از Coroutines در داخل یک
ViewModel
برای واکشی داده ها از شبکه و ذخیره آن در یک پایگاه داده بدون مسدود کردن موضوع اصلی. - و نحوه لغو همه Coroutines پس از اتمام
ViewModel
.
برای آزمایش کد مبتنی بر Coroutine ، ما هم با آزمایش رفتار و هم با استفاده مستقیم از توابع suspend
از آزمایشات را پوشش دادیم.
بیشتر بدانید
برای یادگیری استفاده از Coroutines پیشرفته تر در Android ، " Coroutines Advanced Coroutines را با Flow Kotlin و Livedata " CodeLab بررسی کنید.
کوتلین کوروتین ها ویژگی های بسیاری دارند که تحت پوشش این CodeLab قرار نگرفتند. اگر علاقه مند به کسب اطلاعات بیشتر در مورد Kotlin Coroutines هستید ، راهنماهای Coroutines منتشر شده توسط JetBrains را بخوانید. همچنین برای استفاده بیشتر از الگوهای استفاده بیشتر از Coroutines در Android ، " عملکرد برنامه را با Coroutines Kotlin" بهبود بخشید .