از Kotlin Coroutines در برنامه اندروید خود استفاده کنید

در این لبه کد یاد خواهید گرفت که چگونه از 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 این دستورالعمل ها را دنبال کنید.

  1. اگر فایل فشرده kotlin-coroutines را دانلود کردید، فایل را از حالت فشرده خارج کنید.
  2. پروژه coroutines-codelab را در Android Studio باز کنید.
  3. ماژول start برنامه را انتخاب کنید.
  4. کلیک کنید بر روی execute.png دکمه اجرا را انتخاب کنید یا یک شبیه ساز انتخاب کنید یا دستگاه اندرویدی خود را که باید قابلیت اجرای Android Lollipop را داشته باشد وصل کنید (حداقل SDK پشتیبانی شده 21 است). صفحه Kotlin Coroutines باید ظاهر شود:

این برنامه شروع کننده از رشته‌ها برای افزایش شمارش با تأخیر کوتاهی پس از فشار دادن صفحه استفاده می‌کند. همچنین عنوان جدیدی را از شبکه دریافت می کند و آن را روی صفحه نمایش می دهد. اکنون آن را امتحان کنید و بعد از یک تاخیر کوتاه، تعداد و پیام تغییر می کند. در این کد لبه شما این برنامه را به استفاده از کوروتین ها تبدیل می کنید.

این برنامه از کامپوننت‌های معماری برای جدا کردن کد UI در MainActivity از منطق برنامه در MainViewModel استفاده می‌کند. یک لحظه برای آشنایی با ساختار پروژه وقت بگذارید.

  1. MainActivity رابط کاربری را نمایش می دهد، شنوندگان کلیک را ثبت می کند و می تواند یک Snackbar را نمایش دهد. رویدادها را به MainViewModel می کند و صفحه را بر اساس LiveData در MainViewModel به روز می کند.
  2. MainViewModel رویدادها را در onMainViewClicked مدیریت می کند و با استفاده از LiveData. با MainActivity ارتباط برقرار می کند.
  3. Executors BACKGROUND, را تعریف می کنند که می تواند چیزها را روی یک رشته پس زمینه اجرا کند.
  4. 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")
   }
}

این کد همان کار را انجام می دهد، یک ثانیه منتظر می ماند تا نوار اسنک را نشان دهد. با این حال، چند تفاوت مهم وجود دارد:

  1. viewModelScope. launch یک برنامه در viewModelScope شروع می شود. این به این معنی است که وقتی کاری که به viewModelScope لغو شود، تمام کارهای روتین در این job/scope لغو خواهند شد. اگر کاربر قبل از بازگشت delay ، Activity را ترک کند، زمانی که onCleared پس از تخریب ViewModel فراخوانی شود، این برنامه به طور خودکار لغو می شود.
  2. از آنجایی که viewModelScope یک توزیع کننده پیش فرض Dispatchers.Main دارد، این برنامه در موضوع اصلی راه اندازی می شود. بعداً نحوه استفاده از موضوعات مختلف را خواهیم دید.
  3. 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 را در یک تست خارج از دستگاه آزمایش کنیم از دو قانون استفاده می شود:

  1. InstantTaskExecutorRule یک قانون JUnit است که LiveData را برای اجرای همزمان هر کار پیکربندی می کند.
  2. 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 دارد، لازم نیست یک ثانیه منتظر بمانید تا مقدار تنظیم شود.

تست موجود را اجرا کنید

  1. روی نام کلاس MainViewModelTest در ویرایشگر خود کلیک راست کنید تا منوی زمینه باز شود.
  2. در منوی زمینه را انتخاب کنید execute.png "MainViewModelTest" را اجرا کنید
  3. برای اجراهای بعدی می توانید این پیکربندی آزمایشی را در تنظیمات کناری انتخاب کنید execute.png دکمه در نوار ابزار به طور پیش فرض، پیکربندی MainViewModelTest نامیده می شود.

شما باید قبولی آزمون را ببینید! و اجرای آن باید کمی کمتر از یک ثانیه طول بکشد.

در تمرین بعدی یاد خواهید گرفت که چگونه از یک APIهای پاسخ به تماس موجود به استفاده از کوروتین ها تبدیل کنید.

در این مرحله، تبدیل یک مخزن برای استفاده از کوروتین ها را شروع می کنید. برای انجام این کار، ما کوروتین ها را به ViewModel ، Repository ، Room و Retrofit اضافه می کنیم.

این ایده خوبی است که قبل از اینکه آنها را به استفاده از کوروتین ها تغییر دهیم، درک کنیم که هر بخش از معماری چه مسئولیتی دارد.

  1. MainDatabase یک پایگاه داده را با استفاده از Room پیاده سازی می کند که یک Title را ذخیره و بارگذاری می کند.
  2. MainNetwork یک API شبکه را پیاده سازی می کند که یک عنوان جدید واکشی می کند. از Retrofit برای واکشی عناوین استفاده می کند. Retrofit طوری پیکربندی شده است که به‌طور تصادفی خطاها یا داده‌های ساختگی را برمی‌گرداند، اما در غیر این صورت طوری رفتار می‌کند که گویی درخواست‌های شبکه واقعی را ارائه می‌کند.
  3. TitleRepository یک API واحد را برای واکشی یا به‌روزرسانی عنوان با ترکیب داده‌ها از شبکه و پایگاه داده پیاده‌سازی می‌کند.
  4. 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 همیشه پس از اجرای پرس‌وجو خاموش است.

با انتخاب تنظیمات شروع و سپس فشار دادن مجدد برنامه را اجرا کنید execute.png ، وقتی روی هر جایی ضربه می زنید، باید یک اسپینر در حال بارگذاری را ببینید. عنوان ثابت خواهد ماند زیرا ما هنوز شبکه یا پایگاه داده خود را متصل نکرده ایم.

در تمرین بعدی، مخزن را به‌روزرسانی می‌کنید تا واقعاً کار انجام دهد.

در این تمرین می‌آموزید که چگونه رشته‌ای را که یک کوروتین روی آن اجرا می‌شود تغییر دهید تا یک نسخه کارآمد از 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 برای ارتباط وضعیت بارگیری و خطا به تماس گیرنده پیاده سازی می شود.

این تابع چند کار را برای اجرای تازه سازی انجام می دهد.

  1. با BACKGROUND ExecutorService به رشته دیگری بروید
  2. درخواست شبکه fetchNextTitle را با استفاده از روش blocking execute() . این درخواست شبکه را در رشته فعلی اجرا می‌کند، در این مورد یکی از رشته‌های موجود در BACKGROUND .
  3. اگر نتیجه موفقیت آمیز بود، آن را با insertTitle در پایگاه داده ذخیره کنید و onCompleted() را فراخوانی کنید.
  4. اگر نتیجه موفقیت آمیز نبود، یا استثنا وجود داشت، با متد 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 کامل شود، به حالت تعلیق در می‌آید.

در مقایسه با نسخه برگشت، دو تفاوت مهم وجود دارد:

  1. withContext نتیجه آن را به Dispatcher که آن را فراخوانی کرده است برمی‌گرداند، در این مورد Dispatchers.Main . نسخه برگشت به تماس تماس‌های موجود در یک رشته در سرویس اجراکننده BACKGROUND نامیده می‌شود.
  2. تماس گیرنده نیازی به ارسال یک تماس به این تابع ندارد. آنها می توانند برای دریافت نتیجه یا خطا به تعلیق و از سرگیری اعتماد کنند.

دوباره برنامه را اجرا کنید

اگر دوباره برنامه را اجرا کنید، خواهید دید که اجرای جدید مبتنی بر کوروتین ها نتایج را از شبکه بارگیری می کند!

در مرحله بعدی، کوروتین ها را در 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 باید دو کار را انجام دهید:

  1. یک اصلاح کننده تعلیق به عملکرد اضافه کنید
  2. 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

  1. کلید alt-enter را بزنید تا اصلاح کننده های تعلیق را به همه توابع موجود در هیرانشی اضافه کنید

شبکه اصلی فیک

  1. کلید alt-enter را بزنید تا اصلاح کننده های تعلیق را به همه توابع موجود در هیرانشی اضافه کنید
  2. این تابع را جایگزین fetchNextTitle کنید
override suspend fun fetchNextTitle() = result

شبکه اصلی CompletableFake

  1. کلید alt-enter را بزنید تا اصلاح کننده های تعلیق را به همه توابع موجود در هیرانشی اضافه کنید
  2. این تابع را جایگزین 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.