از 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های موجود به کوروتین بیاموزید.
  • از کوروتین ها با کامپوننت های معماری استفاده کنید.
  • بهترین روش ها را برای آزمایش کوروتین ها بیاموزید.

آنچه شما نیاز دارید

  • 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 خود را که باید قابلیت اجرای Android Lollipop را داشته باشد وصل کنید (حداقل SDK پشتیبانی شده 21 است). صفحه Kotlin Coroutines باید ظاهر شود:

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

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

  1. MainActivity رابط کاربری را نمایش می دهد، شنوندگان کلیک را ثبت می کند و می تواند یک Snackbar را نمایش دهد. رویدادها را به MainViewModel منتقل می کند و صفحه را بر اساس LiveData در MainViewModel به روز می کند.
  2. MainViewModel رویدادها را در onMainViewClicked مدیریت می کند و با استفاده از LiveData با MainActivity ارتباط برقرار می کند LiveData.
  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 { ... }

در بخش بعدی برنامه نمونه کار را با کوروتین ها معرفی خواهید کرد.

در این تمرین شما یک کوروتین می نویسید تا بعد از تاخیر یک پیام نمایش داده شود. برای شروع، مطمئن شوید که ماژول را در 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")
   }
}

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

  1. viewModelScope. launch یک برنامه در viewModelScope شروع می شود. این به این معنی است که وقتی کاری که به viewModelScope ارسال کردیم لغو شود، تمام کارهای روتین در این job/scope لغو خواهند شد. اگر کاربر قبل از بازگشت delay فعالیت را ترک کند، زمانی که 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 است.

نسخه پاسخ به تماس

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 همیشه پس از اجرای پرس‌وجو خاموش است.

با انتخاب تنظیمات شروع و سپس فشار دادن مجدد برنامه را اجرا کنید 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. اگر نتیجه موفقیت آمیز نبود یا استثنا وجود داشت، با متد 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 کامل شود، به حالت تعلیق در می‌آید.

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

  1. withContext نتیجه را به Dispatcher که آن را فراخوانی کرده است برمی‌گرداند، در این مورد Dispatchers.Main . نسخه ی callback callback های موجود در یک رشته در سرویس اجرا کننده 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 اضافه کردن تعلیق تعلیق به همه توابع در heiranchy

شبکه اصلی فیک

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

MainNetworkCompletableFake

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