ใช้ Kotlin Coroutines ในแอป Android

ใน Codelab นี้ คุณจะได้เรียนรู้วิธีใช้ Kotlin Coroutines ในแอป Android ซึ่งเป็นวิธีใหม่ในการจัดการเธรดเบื้องหลังที่ช่วยลดความซับซ้อนของโค้ดโดยลดความจำเป็นในการใช้การเรียกกลับ โครูทีนเป็นฟีเจอร์ของ 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

คุณจะเริ่มต้นด้วยแอปที่มีอยู่ซึ่งสร้างขึ้นโดยใช้ Architecture Components ซึ่งใช้รูปแบบการเรียกกลับสำหรับงานที่ใช้เวลานาน

เมื่อสิ้นสุดโค้ดแล็บนี้ คุณจะมีประสบการณ์มากพอที่จะใช้โครูทีนในแอปเพื่อโหลดข้อมูลจากเครือข่าย และจะสามารถผสานรวมโครูทีนเข้ากับแอปได้ นอกจากนี้ คุณยังจะคุ้นเคยกับแนวทางปฏิบัติแนะนำสำหรับโครูทีน และวิธีเขียนการทดสอบกับโค้ดที่ใช้โครูทีนด้วย

ข้อกำหนดเบื้องต้น

  • คุ้นเคยกับคอมโพเนนต์สถาปัตยกรรม ViewModel, LiveData, Repository และ Room
  • มีประสบการณ์เกี่ยวกับไวยากรณ์ Kotlin รวมถึงฟังก์ชันส่วนขยายและแลมบ์ดา
  • ความเข้าใจพื้นฐานเกี่ยวกับการใช้เธรดใน Android ซึ่งรวมถึงเธรดหลัก เธรดเบื้องหลัง และการเรียกกลับ

สิ่งที่คุณต้องทำ

  • เรียกใช้โค้ดที่เขียนด้วยโครูทีนและรับผลลัพธ์
  • ใช้ฟังก์ชันระงับเพื่อให้โค้ดแบบอะซิงโครนัสเป็นแบบลำดับ
  • ใช้ launch และ runBlocking เพื่อควบคุมวิธีเรียกใช้โค้ด
  • ดูเทคนิคในการแปลง API ที่มีอยู่เป็นโครูทีนโดยใช้ suspendCoroutine
  • ใช้โครูทีนกับคอมโพเนนต์สถาปัตยกรรม
  • ดูแนวทางปฏิบัติแนะนำสำหรับการทดสอบโครูทีน

สิ่งที่คุณต้องมี

  • Android Studio 3.5 (Codelab อาจใช้ได้กับเวอร์ชันอื่นๆ แต่บางอย่างอาจขาดหายไปหรือดูแตกต่างออกไป)

หากพบปัญหา (ข้อบกพร่องของโค้ด ข้อผิดพลาดทางไวยากรณ์ คำที่ไม่ชัดเจน ฯลฯ) ขณะทำตาม Codelab นี้ โปรดรายงานปัญหาผ่านลิงก์รายงานข้อผิดพลาดที่มุมซ้ายล่างของ Codelab

ดาวน์โหลดโค้ด

คลิกลิงก์ต่อไปนี้เพื่อดาวน์โหลดโค้ดทั้งหมดสำหรับ Codelab นี้

ดาวน์โหลด Zip

... หรือโคลนที่เก็บ GitHub จากบรรทัดคำสั่งโดยใช้คำสั่งต่อไปนี้

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

คำถามที่พบบ่อย

ก่อนอื่น มาดูแอปตัวอย่างเริ่มต้นกัน ทำตามวิธีการเหล่านี้เพื่อเปิดแอปตัวอย่างใน Android Studio

  1. หากดาวน์โหลดไฟล์ ZIP kotlin-coroutines ให้แตกไฟล์
  2. เปิดcoroutines-codelabโปรเจ็กต์ใน Android Studio
  3. เลือกโมดูลแอปพลิเคชัน start
  4. คลิกปุ่มexecute.pngเรียกใช้ แล้วเลือกโปรแกรมจำลองหรือเชื่อมต่ออุปกรณ์ Android ซึ่งต้องสามารถเรียกใช้ Android Lollipop ได้ (SDK ขั้นต่ำที่รองรับคือ 21) หน้าจอ Kotlin Coroutines ควรปรากฏขึ้น

แอปเริ่มต้นนี้ใช้เธรดเพื่อเพิ่มจำนวนโดยมีการหน่วงเวลาสั้นๆ หลังจากที่คุณกดหน้าจอ นอกจากนี้ยังจะดึงชื่อใหม่จากเครือข่ายและแสดงบนหน้าจอด้วย ลองเลยตอนนี้ แล้วคุณจะเห็นว่าจำนวนและข้อความเปลี่ยนไปหลังจากผ่านไปครู่หนึ่ง ใน Codelab นี้ คุณจะแปลงแอปพลิเคชันนี้ให้ใช้โคโรทีน

แอปนี้ใช้คอมโพเนนต์สถาปัตยกรรมเพื่อแยกโค้ด UI ใน MainActivity ออกจากตรรกะของแอปพลิเคชันใน MainViewModel โปรดสละเวลาสักครู่เพื่อทำความคุ้นเคยกับโครงสร้างของโปรเจ็กต์

  1. MainActivity แสดง UI ลงทะเบียน Listener การคลิก และแสดง Snackbar ได้ โดยจะส่งเหตุการณ์ไปยัง MainViewModel และอัปเดตหน้าจอตาม LiveData ใน MainViewModel
  2. MainViewModel จัดการเหตุการณ์ใน onMainViewClicked และจะสื่อสารกับ MainActivity โดยใช้ LiveData.
  3. Executors defines BACKGROUND, ซึ่งสามารถเรียกใช้สิ่งต่างๆ ในเธรดเบื้องหลังได้
  4. TitleRepository จะดึงข้อมูลผลลัพธ์จากเครือข่ายและบันทึกลงในฐานข้อมูล

การเพิ่มโครูทีนลงในโปรเจ็กต์

หากต้องการใช้โครูทีนใน Kotlin คุณต้องรวมไลบรารี coroutines-core ไว้ในไฟล์ build.gradle (Module: app) ของโปรเจ็กต์ โปรเจ็กต์ Codelab ได้ดำเนินการนี้ให้คุณแล้ว จึงไม่จำเป็นต้องทำเพื่อทำ Codelab ให้เสร็จสมบูรณ์

Coroutines ใน Android พร้อมใช้งานเป็นไลบรารีหลักและส่วนขยายเฉพาะของ Android ดังนี้

  • kotlinx-corountines-core — อินเทอร์เฟซหลักสำหรับการใช้โครูทีนใน Kotlin
  • kotlinx-coroutines-android — การรองรับเธรดหลักของ Android ใน Coroutine

แอปเริ่มต้นมีทรัพยากร Dependency อยู่แล้วใน build.gradle.เมื่อสร้างโปรเจ็กต์แอปใหม่ คุณจะต้องเปิด build.gradle (Module: app) และเพิ่มทรัพยากร Dependency ของโครูทีนลงในโปรเจ็กต์

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

ใน Android คุณต้องหลีกเลี่ยงการบล็อกเทรดหลัก เทรดหลักคือเทรดเดียวที่จัดการการอัปเดตทั้งหมดใน UI นอกจากนี้ยังเป็นเทรดที่เรียกตัวแฮนเดิลการคลิกและโค้ดเรียกกลับอื่นๆ ของ UI ทั้งหมดด้วย ดังนั้นจึงต้องทำงานได้อย่างราบรื่นเพื่อรับประกันประสบการณ์ของผู้ใช้ที่ยอดเยี่ยม

เพื่อให้แอปแสดงต่อผู้ใช้โดยไม่มีการหยุดชั่วคราวที่มองเห็นได้ เทรดหลักต้องอัปเดตหน้าจอทุกๆ 16 มิลลิวินาทีขึ้นไป ซึ่งเท่ากับประมาณ 60 เฟรมต่อวินาที งานทั่วไปหลายอย่างใช้เวลานานกว่านี้ เช่น การแยกวิเคราะห์ชุดข้อมูล JSON ขนาดใหญ่ การเขียนข้อมูลลงในฐานข้อมูล หรือการดึงข้อมูลจากเครือข่าย ดังนั้น การเรียกใช้โค้ดแบบนี้จากเทรดหลักอาจทำให้แอปหยุดชั่วคราว กระตุก หรือค้างได้ และหากบล็อกเทรดหลักนานเกินไป แอปอาจขัดข้องและแสดงกล่องโต้ตอบแอปพลิเคชันไม่ตอบสนอง

ดูวิดีโอด้านล่างเพื่อดูข้อมูลเบื้องต้นเกี่ยวกับวิธีที่โครูทีนช่วยแก้ปัญหานี้ใน Android โดยการนำความปลอดภัยของเทรดหลักมาใช้

รูปแบบการเรียกกลับ

รูปแบบหนึ่งในการทำงานที่ใช้เวลานานโดยไม่บล็อกเทรดหลักคือการเรียกกลับ การใช้การเรียกกลับช่วยให้คุณเริ่มงานที่ใช้เวลานานในเธรดเบื้องหลังได้ เมื่องานเสร็จสมบูรณ์ ระบบจะเรียกใช้การเรียกกลับเพื่อแจ้งผลลัพธ์ในเทรดหลัก

ดูตัวอย่างรูปแบบการเรียกกลับ

// 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) Callback ช่วยให้ slowFetch ทำงานในเธรดเบื้องหลังและแสดงผลเมื่อพร้อม

การใช้โครูทีนเพื่อนำการเรียกกลับออก

แม้ว่า Callback จะเป็นรูปแบบที่ดี แต่ก็มีข้อเสียอยู่บ้าง โค้ดที่ใช้การเรียกกลับอย่างหนักอาจอ่านได้ยากและให้เหตุผลได้ยากขึ้น นอกจากนี้ การเรียกกลับยังไม่อนุญาตให้ใช้ฟีเจอร์ภาษาบางอย่าง เช่น ข้อยกเว้น

โคโรทีน Kotlin ช่วยให้คุณแปลงโค้ดที่อิงตามการเรียกกลับเป็นโค้ดแบบลำดับได้ โดยปกติแล้ว โค้ดที่เขียนตามลำดับจะอ่านง่ายกว่า และยังใช้ฟีเจอร์ภาษา เช่น ข้อยกเว้น ได้ด้วย

ในท้ายที่สุด ทั้ง 2 อย่างนี้จะทำสิ่งเดียวกัน นั่นคือรอจนกว่าจะมีผลลัพธ์จากงานที่ใช้เวลานานและดำเนินการต่อ แต่ในโค้ดจะดูแตกต่างกันมาก

คีย์เวิร์ด suspend เป็นวิธีของ Kotlin ในการทำเครื่องหมายฟังก์ชันหรือประเภทฟังก์ชันที่ใช้ได้กับโครูทีน เมื่อโครูทีนเรียกใช้ฟังก์ชันที่ทำเครื่องหมาย 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 { ... }

เช่นเดียวกับเวอร์ชัน Callback makeNetworkRequest ต้องแสดงผลจากเทรดหลักทันทีเนื่องจากมีการทำเครื่องหมายเป็น @UiThread ซึ่งหมายความว่าโดยปกติแล้วจะเรียกใช้เมธอดการบล็อก เช่น slowFetch ไม่ได้ suspend คีย์เวิร์ดจะช่วยให้คุณทำสิ่งนี้ได้

เมื่อเทียบกับโค้ดที่ใช้การเรียกกลับ โค้ดโครูทีนจะให้ผลลัพธ์เดียวกันในการยกเลิกการบล็อกเธรดปัจจุบันโดยใช้โค้ดน้อยกว่า เนื่องจากมีรูปแบบตามลำดับ คุณจึงเชื่อมโยงงานที่ใช้เวลานานหลายงานได้ง่ายๆ โดยไม่ต้องสร้างการเรียกกลับหลายรายการ เช่น โค้ดที่ดึงผลลัพธ์จากปลายทางเครือข่าย 2 แห่งและบันทึกลงในฐานข้อมูลสามารถเขียนเป็นฟังก์ชันในโครูทีนโดยไม่มีการเรียกกลับ อย่างเช่นดังนี้

// 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 ขอบเขตจะควบคุมอายุการใช้งานของโครูทีนผ่านงานของโครูทีน เมื่อยกเลิกงานของขอบเขต ระบบจะยกเลิกโครูทีนทั้งหมดที่เริ่มต้นในขอบเขตนั้น ใน Android คุณสามารถใช้ขอบเขตเพื่อยกเลิกโครูทีนที่กำลังทำงานทั้งหมดได้ เช่น เมื่อผู้ใช้นำทางออกจาก Activity หรือ Fragment นอกจากนี้ ขอบเขตยังช่วยให้คุณระบุ Dispatcher เริ่มต้นได้ด้วย Dispatcher จะควบคุมว่าเธรดใดที่เรียกใช้โครูทีน

สำหรับโครูทีนที่ UI เริ่มต้น โดยปกติแล้วการเริ่มต้นโครูทีนใน Dispatchers.Main ซึ่งเป็นเทรดหลักใน Android จะถูกต้อง โครูทีนที่เริ่มต้นใน Dispatchers.Main จะไม่บล็อกเทรดหลักขณะที่ระงับ เนื่องจากโครูทีน ViewModel มักจะอัปเดต UI ในเทรดหลักเสมอ การเริ่มโครูทีนในเทรดหลักจึงช่วยให้คุณไม่ต้องสลับเทรดเพิ่มเติม โครูทีนที่เริ่มต้นในเทรดหลักจะเปลี่ยน Dispatcher ได้ทุกเมื่อหลังจากที่เริ่มต้น เช่น สามารถใช้ Dispatcher อื่นเพื่อแยกวิเคราะห์ผลลัพธ์ JSON ขนาดใหญ่จากเทรดหลักได้

การใช้ viewModelScope

ไลบรารี AndroidX lifecycle-viewmodel-ktx จะเพิ่ม CoroutineScope ลงใน ViewModel ที่กำหนดค่าให้เริ่มโครูทีนที่เกี่ยวข้องกับ 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 บล็อกเทรดปัจจุบัน จึงจะทำให้ UI หยุดทำงานหากมีการเรียกใช้ในเทรดหลัก 1 วินาทีหลังจากที่ผู้ใช้คลิกมุมมองหลัก ระบบจะส่งคำขอแถบแสดงข้อความ

คุณสามารถดูได้โดยนำ BACKGROUND ออกจากโค้ดแล้วเรียกใช้โค้ดอีกครั้ง วงกลมโหลดจะไม่แสดงและทุกอย่างจะ "กระโดด" ไปยังสถานะสุดท้ายในอีก 1 วินาทีต่อมา

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 วินาทีก่อนแสดง Snackbar แต่มีความแตกต่างที่สำคัญบางประการดังนี้

  1. viewModelScope.launch จะเริ่มโครูทีนใน viewModelScope ซึ่งหมายความว่าเมื่อมีการยกเลิกงานที่เราส่งไปยัง viewModelScope ระบบจะยกเลิกโครูทีนทั้งหมดในงาน/ขอบเขตนี้ หากผู้ใช้ออกจากกิจกรรมก่อนที่ delay จะแสดงผล ระบบจะยกเลิกโครูทีนนี้โดยอัตโนมัติเมื่อมีการเรียก onCleared เมื่อ ViewModel ถูกทำลาย
  2. เนื่องจาก viewModelScope มีตัวจัดส่งเริ่มต้นเป็น Dispatchers.Main ดังนั้นระบบจะเปิดใช้โครูทีนนี้ในเทรดหลัก เราจะมาดูวิธีใช้เธรดต่างๆ ในภายหลัง
  3. ฟังก์ชัน delay คือฟังก์ชัน suspend ซึ่งจะแสดงใน Android Studio โดยไอคอน ในแถบด้านซ้าย แม้ว่าโครูทีนนี้จะทำงานในเทรดหลัก แต่ delay จะไม่บล็อกเทรดเป็นเวลา 1 วินาที แต่ตัวจัดสรรจะกำหนดเวลาให้โครูทีนกลับมาทำงานต่อในอีก 1 วินาทีที่คำสั่งถัดไป

เรียกใช้ได้เลย เมื่อคลิกมุมมองหลัก คุณจะเห็นแถบแสดงข้อความหนึ่งวินาทีต่อมา

ในส่วนถัดไป เราจะพิจารณาวิธีทดสอบฟังก์ชันนี้

ในแบบฝึกหัดนี้ คุณจะได้เขียนการทดสอบสำหรับโค้ดที่เพิ่งเขียน แบบฝึกหัดนี้จะแสดงวิธีทดสอบโครูทีนที่ทำงานบน 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 เราใช้กฎ 2 ข้อเพื่อให้ทดสอบ MainViewModel ในการทดสอบนอกอุปกรณ์ได้ ดังนี้

  1. InstantTaskExecutorRule เป็นกฎ JUnit ที่กำหนดค่า LiveData ให้เรียกใช้งานแต่ละงานแบบพร้อมกัน
  2. MainCoroutineScopeRule เป็นกฎที่กำหนดเองในฐานของโค้ดนี้ซึ่งกำหนดค่า Dispatchers.Main ให้ใช้ TestCoroutineDispatcher จาก kotlinx-coroutines-test ซึ่งจะช่วยให้การทดสอบเลื่อนนาฬิกาเสมือนสำหรับการทดสอบ และช่วยให้โค้ดใช้ Dispatchers.Main ในการทดสอบหน่วยได้

ในเมธอด setup จะมีการสร้างอินสแตนซ์ใหม่ของ MainViewModel โดยใช้การทดสอบแบบจำลอง ซึ่งเป็นการติดตั้งใช้งานเครือข่ายและฐานข้อมูลแบบจำลองที่ระบุไว้ในโค้ดเริ่มต้นเพื่อช่วยเขียนการทดสอบโดยไม่ต้องใช้เครือข่ายหรือฐานข้อมูลจริง

สำหรับการทดสอบนี้ เราต้องการใช้การจำลองเพื่อตอบสนองการขึ้นต่อกันของ MainViewModel เท่านั้น ในโค้ดแล็บนี้ คุณจะได้อัปเดตการจำลองเพื่อรองรับโครูทีนในภายหลัง

เขียนการทดสอบที่ควบคุมโครูทีน

เพิ่มการทดสอบใหม่เพื่อให้แน่ใจว่าการแตะจะได้รับการอัปเดต 1 วินาทีหลังจากคลิกมุมมองหลัก

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 จะเป็นการเปิดใช้โครูทีนที่เราเพิ่งสร้าง การทดสอบนี้จะตรวจสอบว่าข้อความการแตะยังคงเป็น "0 การแตะ" ทันทีหลังจากเรียกใช้ onMainViewClicked จากนั้น 1 วินาทีต่อมาข้อความจะอัปเดตเป็น "1 การแตะ"

การทดสอบนี้ใช้เวลาเสมือนเพื่อควบคุมการดำเนินการของโครูทีนที่เปิดใช้โดย onMainViewClicked MainCoroutineScopeRule ช่วยให้คุณหยุดชั่วคราว กลับมาใช้งานต่อ หรือควบคุมการดำเนินการของโครูทีนที่เปิดตัวใน Dispatchers.Main ในที่นี้ เราเรียกใช้ advanceTimeBy(1_000) ซึ่งจะทำให้ Dispatcher หลักเรียกใช้โครูทีนที่กำหนดเวลาให้กลับมาทำงานต่อในอีก 1 วินาทีต่อมาทันที

การทดสอบนี้เป็นแบบดีเทอร์มินิสติกอย่างสมบูรณ์ ซึ่งหมายความว่าจะทำงานในลักษณะเดียวกันเสมอ และเนื่องจากมีสิทธิ์ควบคุมการดำเนินการของโครูทีนที่เปิดตัวใน Dispatchers.Main อย่างเต็มที่ จึงไม่จำเป็นต้องรอ 1 วินาทีเพื่อให้ตั้งค่า

เรียกใช้การทดสอบที่มีอยู่

  1. คลิกขวาที่ชื่อคลาส MainViewModelTest ในโปรแกรมแก้ไขเพื่อเปิดเมนูตามบริบท
  2. ในเมนูตามบริบท ให้เลือก execute.pngRun 'MainViewModelTest'
  3. สำหรับการทดสอบในอนาคต คุณสามารถเลือกการกำหนดค่าการทดสอบนี้ในการกำหนดค่าข้างปุ่ม execute.png ในแถบเครื่องมือ โดยค่าเริ่มต้น ระบบจะเรียกการกำหนดค่าว่า MainViewModelTest

คุณควรเห็นว่าการทดสอบผ่าน และควรใช้เวลาไม่ถึง 1 วินาทีในการเรียกใช้

ในแบบฝึกหัดถัดไป คุณจะได้เรียนรู้วิธีแปลงจาก 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)
       }
   })
}

ฟังก์ชันนี้จะเรียกใช้ทุกครั้งที่ผู้ใช้คลิกบนหน้าจอ และจะทำให้ที่เก็บรีเฟรชชื่อและเขียนชื่อใหม่ลงในฐานข้อมูล

การติดตั้งใช้งานนี้ใช้การเรียกกลับเพื่อทำสิ่งต่อไปนี้

  • ก่อนที่จะเริ่มการค้นหา ระบบจะแสดงไอคอนหมุนของการโหลดพร้อมข้อความ _spinner.value = true
  • เมื่อได้รับผลลัพธ์แล้ว ฟังก์ชันจะล้างวงกลมหมุนที่กำลังโหลดด้วย _spinner.value = false
  • หากได้รับข้อผิดพลาด ฟังก์ชันนี้จะบอกให้ Snackbar แสดงและล้าง Spinner

โปรดทราบว่าระบบจะไม่ส่ง title ไปยังonCompleted เนื่องจากเราเขียนชื่อทั้งหมดลงในฐานข้อมูล Room UI จึงอัปเดตชื่อปัจจุบันโดยสังเกต LiveData ที่อัปเดตโดย Room

ในการอัปเดตโครูทีน เราจะคงลักษณะการทำงานเดิมไว้ แนวทางที่ดีคือการใช้แหล่งข้อมูลที่สังเกตได้ เช่น Room ฐานข้อมูล เพื่อให้ UI เป็นข้อมูลล่าสุดอยู่เสมอโดยอัตโนมัติ

เวอร์ชันโครูทีน

มาเขียน refreshTitle ใหม่ด้วยโครูทีนกัน

เนื่องจากเราจะต้องใช้ฟังก์ชันนี้ทันที ให้สร้างฟังก์ชันระงับที่ว่างเปล่าในที่เก็บของเรา (TitleRespository.kt) กำหนดฟังก์ชันใหม่ที่ใช้ตัวดำเนินการ suspend เพื่อบอก Kotlin ว่าฟังก์ชันนี้ทำงานร่วมกับโครูทีน

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

เมื่อทำ Codelab นี้เสร็จแล้ว คุณจะอัปเดต Codelab นี้เพื่อใช้ Retrofit และ Room ในการดึงข้อมูลชื่อใหม่และเขียนลงในฐานข้อมูลโดยใช้ Coroutine ตอนนี้มันจะใช้เวลา 500 มิลลิวินาทีในการแสร้งว่าทำงานแล้วจึงดำเนินการต่อ

ใน MainViewModel ให้แทนที่เวอร์ชันการเรียกกลับของ 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 เมื่อผู้ใช้เลื่อนออกจากหน้าจอนี้ ระบบจะยกเลิกงานที่โครูทีนนี้เริ่มต้นโดยอัตโนมัติ ซึ่งหมายความว่าจะไม่สร้างคำขอเครือข่ายหรือการค้นหาฐานข้อมูลเพิ่มเติม

โค้ด 2-3 บรรทัดถัดไปจะเรียกใช้ refreshTitle ใน repository

try {
    _spinner.value = true
    repository.refreshTitle()
}

ก่อนที่โครูทีนนี้จะทำอะไรก็ตาม โครูทีนจะเริ่มไอคอนหมุนแสดงการโหลด จากนั้นจะเรียกใช้ refreshTitle เหมือนกับฟังก์ชันปกติ อย่างไรก็ตาม เนื่องจาก refreshTitle เป็นฟังก์ชันที่ระงับ จึงทำงานแตกต่างจากฟังก์ชันปกติ

เราไม่จำเป็นต้องส่งการเรียกกลับ โครูทีนจะหยุดชั่วคราวจนกว่า refreshTitle จะกลับมาทำงานต่อ แม้ว่าดูเหมือนการเรียกฟังก์ชันการบล็อกปกติ แต่ฟังก์ชันนี้จะรอโดยอัตโนมัติจนกว่าการค้นหาเครือข่ายและฐานข้อมูลจะเสร็จสมบูรณ์ก่อนที่จะกลับมาทำงานต่อโดยไม่บล็อกเธรดหลัก

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

ข้อยกเว้นในฟังก์ชันระงับจะทำงานเหมือนกับข้อผิดพลาดในฟังก์ชันปกติ หากคุณส่งข้อผิดพลาดในฟังก์ชันระงับ ระบบจะส่งข้อผิดพลาดไปยังผู้เรียก ดังนั้นแม้ว่าการดำเนินการจะแตกต่างกันอย่างมาก แต่คุณก็สามารถใช้บล็อก try/catch ปกติเพื่อจัดการได้ ซึ่งจะเป็นประโยชน์เนื่องจากช่วยให้คุณใช้การรองรับภาษาในตัวสำหรับการจัดการข้อผิดพลาดได้แทนที่จะสร้างการจัดการข้อผิดพลาดที่กำหนดเองสำหรับทุกการเรียกกลับ

และหากคุณส่งข้อยกเว้นออกจากโครูทีน โครูทีนนั้นจะยกเลิกโครูทีนหลักโดยค่าเริ่มต้น ซึ่งหมายความว่าคุณยกเลิกงานที่เกี่ยวข้องหลายอย่างพร้อมกันได้ง่ายๆ

จากนั้นในบล็อก finally เราจะตรวจสอบว่าได้ปิดสปินเนอร์ทุกครั้งหลังจากเรียกใช้การค้นหา

เรียกใช้แอปพลิเคชันอีกครั้งโดยเลือกการกำหนดค่าเริ่ม แล้วกดexecute.png คุณควรเห็นวงกลมโหลดเมื่อแตะที่ใดก็ได้ ชื่อจะยังคงเหมือนเดิมเนื่องจากเรายังไม่ได้เชื่อมต่อเครือข่ายหรือฐานข้อมูล

ในแบบฝึกหัดถัดไป คุณจะอัปเดตรีพอสิทอรีเพื่อทำงานจริง

ในแบบฝึกหัดนี้ คุณจะได้เรียนรู้วิธีเปลี่ยนเธรดที่โครูทีนทำงานเพื่อใช้เวอร์ชันที่ใช้งานได้ของ TitleRepository

ตรวจสอบโค้ดเรียกกลับที่มีอยู่ใน refreshTitle

เปิด TitleRepository.kt แล้วตรวจสอบการติดตั้งใช้งานที่อิงตามการเรียกกลับที่มีอยู่

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 จะได้รับการติดตั้งใช้งานด้วยการเรียกกลับเพื่อสื่อสารสถานะการโหลดและข้อผิดพลาดกับผู้เรียก

ฟังก์ชันนี้จะทำหลายอย่างเพื่อใช้การรีเฟรช

  1. เปลี่ยนไปใช้ชุดข้อความอื่นด้วย BACKGROUND ExecutorService
  2. เรียกใช้fetchNextTitleคำขอเครือข่ายโดยใช้วิธีการบล็อก execute() ซึ่งจะเรียกใช้คำขอเครือข่ายในเธรดปัจจุบัน ในกรณีนี้คือเธรดใดเธรดหนึ่งใน BACKGROUND
  3. หากผลลัพธ์สำเร็จ ให้บันทึกลงในฐานข้อมูลด้วย insertTitle และเรียกใช้เมธอด onCompleted()
  4. หากผลลัพธ์ไม่สำเร็จหรือมีข้อยกเว้น ให้เรียกใช้เมธอด onError เพื่อแจ้งให้ผู้เรียกทราบว่าการรีเฟรชไม่สำเร็จ

การติดตั้งใช้งานที่อิงตามการเรียกกลับนี้เป็นเทรดหลักที่ปลอดภัยเนื่องจากจะไม่บล็อกเทรดหลัก แต่ต้องใช้การเรียกกลับเพื่อแจ้งให้ผู้โทรทราบเมื่อการทำงานเสร็จสมบูรณ์ นอกจากนี้ ยังเรียกใช้การเรียกกลับในเธรด BACKGROUND ที่สลับไปด้วย

การบล็อกการเรียกจากโครูทีน

เราสามารถทำให้โค้ดนี้ปลอดภัยในเทรดหลักได้โดยใช้โครูทีนโดยไม่ต้องใช้โครูทีนกับเครือข่ายหรือฐานข้อมูล ซึ่งจะช่วยให้เราไม่ต้องใช้การเรียกกลับและส่งผลลัพธ์กลับไปยังเธรดที่เรียกใช้ฟังก์ชันนี้ในตอนแรกได้

คุณสามารถใช้รูปแบบนี้ได้ทุกเมื่อที่ต้องการบล็อกหรือทำงานที่ใช้ CPU สูงจากภายในโครูทีน เช่น การจัดเรียงและการกรองรายการขนาดใหญ่ หรือการอ่านจากดิสก์

หากต้องการสลับระหว่าง Dispatcher ใดๆ โครูทีนจะใช้ withContext การเรียกใช้ withContext จะเปลี่ยนไปใช้ Dispatcher อื่น เฉพาะสำหรับ Lambda จากนั้นจะกลับมาที่ Dispatcher ที่เรียกใช้พร้อมผลลัพธ์ของ Lambda นั้น

โดยค่าเริ่มต้น โครูทีน Kotlin จะมี Dispatcher 3 รายการ ได้แก่ 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)
       }
   }
}

การติดตั้งใช้งานนี้ใช้การเรียกที่บล็อกสำหรับเครือข่ายและฐานข้อมูล แต่ก็ยังง่ายกว่าเวอร์ชัน Callback อยู่เล็กน้อย

โค้ดนี้ยังใช้การเรียก blocking การเรียกใช้ execute() และ insertTitle(...) จะบล็อกทั้งเธรดที่โครูทีนนี้ทำงานอยู่ อย่างไรก็ตาม การเปลี่ยนไปใช้ Dispatchers.IO โดยใช้ withContext จะบล็อกเธรดหนึ่งในตัวจัดสรร IO โครูทีนที่เรียกใช้ฟังก์ชันนี้ ซึ่งอาจทำงานใน Dispatchers.Main จะถูกระงับจนกว่าแลมบ์ดา withContext จะทำงานเสร็จ

เมื่อเทียบกับเวอร์ชันการเรียกกลับ จะมีความแตกต่างที่สำคัญ 2 ประการดังนี้

  1. withContext จะส่งผลลัพธ์กลับไปยัง Dispatcher ที่เรียกใช้ ซึ่งในกรณีนี้คือ Dispatchers.Main เวอร์ชันการเรียกกลับจะเรียกการเรียกกลับในเธรดในBACKGROUNDบริการตัวดำเนินการ
  2. ผู้โทรไม่จำเป็นต้องส่ง Callback ไปยังฟังก์ชันนี้ โดยผู้ใช้สามารถใช้ฟังก์ชันระงับและกลับมาทำงานต่อเพื่อดูผลลัพธ์หรือข้อผิดพลาด

เรียกใช้แอปอีกครั้ง

หากเรียกใช้แอปอีกครั้ง คุณจะเห็นว่าการใช้งานใหม่ที่อิงตามโครูทีนกำลังโหลดผลลัพธ์จากเครือข่าย

ในขั้นตอนถัดไป คุณจะผสานรวมโครูทีนเข้ากับ Room และ Retrofit

เราจะใช้การรองรับฟังก์ชันระงับใน Room และ Retrofit เวอร์ชันเสถียรเพื่อผสานรวม Coroutines ต่อไป จากนั้นจะลดความซับซ้อนของโค้ดที่เราเพิ่งเขียนไปอย่างมากโดยใช้ฟังก์ชันระงับ

โครูทีนใน Room

ก่อนอื่น ให้เปิด MainDatabase.kt แล้วทำให้ insertTitle เป็นฟังก์ชันระงับ

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

เมื่อคุณทำเช่นนี้ Room จะทำให้การค้นหาของคุณปลอดภัยในเทรดหลักและเรียกใช้ในเทรดเบื้องหลังโดยอัตโนมัติ แต่ก็หมายความว่าคุณจะเรียกใช้การค้นหานี้ได้จากภายในโครูทีนเท่านั้น

และนี่คือทั้งหมดที่คุณต้องทำเพื่อใช้โครูทีนใน Room เจ๋งไปเลย

โครูทีนใน Retrofit

ต่อไปเรามาดูวิธีผสานรวมโครูทีนกับ Retrofit กัน เปิด MainNetwork.kt แล้วเปลี่ยน fetchNextTitle เป็นฟังก์ชันระงับ

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 คุณต้องทำ 2 สิ่งต่อไปนี้

  1. เพิ่มตัวแก้ไขระงับลงในฟังก์ชัน
  2. นำ Wrapper Call ออกจากประเภทการคืนค่า ในที่นี้เราจะแสดงผล String แต่คุณจะแสดงผลประเภทที่ซับซ้อนซึ่งมี JSON เป็นข้อมูลสำรองก็ได้ หากยังต้องการให้สิทธิ์เข้าถึง Result แบบเต็มของ Retrofit คุณสามารถแสดงผล Result<String> แทน String จากฟังก์ชันระงับได้

Retrofit จะทำให้ฟังก์ชันระงับเป็นเมน-เซฟโดยอัตโนมัติเพื่อให้คุณเรียกใช้ฟังก์ชันเหล่านั้นได้โดยตรงจาก Dispatchers.Main

การใช้ Room และ Retrofit

ตอนนี้ Room และ Retrofit รองรับฟังก์ชันระงับแล้ว เราจึงใช้ฟังก์ชันเหล่านี้จากที่เก็บได้ เปิด TitleRepository.kt แล้วดูว่าการใช้ฟังก์ชันระงับช่วยลดความซับซ้อนของตรรกะได้อย่างไร แม้จะเทียบกับเวอร์ชันที่บล็อกก็ตาม

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)
   }
}

ว้าว สั้นลงเยอะเลย เกิดอะไรขึ้น การใช้การระงับและกลับมาทำงานต่อทำให้โค้ดสั้นลงได้มาก Retrofit ช่วยให้เราใช้ประเภทการคืนค่า เช่น String หรือออบเจ็กต์ User ที่นี่แทน Call ได้ ซึ่งทำได้อย่างปลอดภัยเนื่องจากภายในฟังก์ชันระงับ Retrofit สามารถเรียกใช้คำขอเครือข่ายในเธรดเบื้องหลังและกลับมาทำงานต่อเมื่อการเรียกใช้เสร็จสมบูรณ์

และที่ดียิ่งกว่านั้นคือเราได้นำwithContextออกไปแล้ว เนื่องจากทั้ง Room และ Retrofit มีฟังก์ชันการระงับเธรดหลักอย่างปลอดภัย คุณจึงสามารถจัดระเบียบงานแบบอะซิงโครนัสนี้จาก Dispatchers.Main ได้อย่างปลอดภัย

การแก้ไขข้อผิดพลาดของคอมไพเลอร์

การเปลี่ยนไปใช้โครูทีนเกี่ยวข้องกับการเปลี่ยนลายเซ็นของฟังก์ชัน เนื่องจากคุณเรียกฟังก์ชันระงับจากฟังก์ชันปกติไม่ได้ เมื่อคุณเพิ่มตัวแก้ไข suspend ในขั้นตอนนี้ ระบบจะสร้างข้อผิดพลาดของคอมไพเลอร์ 2-3 รายการที่แสดงให้เห็นว่าจะเกิดอะไรขึ้นหากคุณเปลี่ยนฟังก์ชันให้ระงับในโปรเจ็กต์จริง

ดูโปรเจ็กต์และแก้ไขข้อผิดพลาดของคอมไพเลอร์โดยเปลี่ยนฟังก์ชันเป็นฟังก์ชันที่สร้างขึ้นเพื่อระงับ วิธีแก้ปัญหาเบื้องต้นสำหรับแต่ละกรณีมีดังนี้

TestingFakes.kt

อัปเดตการทดสอบข้อมูลปลอมเพื่อรองรับตัวแก้ไขการระงับใหม่

TitleDaoFake

  1. กด Alt+Enter เพื่อเพิ่มตัวแก้ไขระงับไปยังฟังก์ชันทั้งหมดในลำดับชั้น

MainNetworkFake

  1. กด Alt+Enter เพื่อเพิ่มตัวแก้ไขระงับไปยังฟังก์ชันทั้งหมดในลำดับชั้น
  2. แทนที่ fetchNextTitle ด้วยฟังก์ชันนี้
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. กด Alt+Enter เพื่อเพิ่มตัวแก้ไขระงับไปยังฟังก์ชันทั้งหมดในลำดับชั้น
  2. แทนที่ fetchNextTitle ด้วยฟังก์ชันนี้
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • ลบฟังก์ชัน refreshTitleWithCallbacks เนื่องจากไม่ได้ใช้งานแล้ว

เรียกใช้แอป

เรียกใช้แอปอีกครั้ง เมื่อคอมไพล์แล้ว คุณจะเห็นว่าแอปกำลังโหลดข้อมูลโดยใช้ Coroutine ตั้งแต่ ViewModel ไปจนถึง Room และ Retrofit

ยินดีด้วย คุณเปลี่ยนแอปนี้ให้ใช้โครูทีนได้สำเร็จแล้ว และสุดท้าย เราจะพูดถึงวิธีทดสอบสิ่งที่เราเพิ่งทำไป

ในแบบฝึกหัดนี้ คุณจะได้เขียนการทดสอบที่เรียกใช้suspendฟังก์ชันโดยตรง

เนื่องจาก refreshTitle มีการเปิดเผยเป็น API สาธารณะ จึงจะมีการทดสอบโดยตรง ซึ่งจะแสดงวิธีเรียกใช้ฟังก์ชันโครูทีนจากการทดสอบ

นี่คือฟังก์ชัน 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 ซึ่งมีรายการสิ่งที่ต้องทำ 2 รายการ

ลองโทรหา refreshTitle จากการทดสอบครั้งแรก whenRefreshTitleSuccess_insertsRows

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

เนื่องจาก refreshTitle เป็นฟังก์ชัน suspend Kotlin จึงไม่ทราบวิธีเรียกใช้ฟังก์ชันดังกล่าว ยกเว้นจากโครูทีนหรือฟังก์ชันระงับอื่น และคุณจะได้รับข้อผิดพลาดของคอมไพเลอร์ เช่น "ฟังก์ชันระงับ refreshTitle ควรเรียกใช้จากโครูทีนหรือฟังก์ชันระงับอื่นเท่านั้น"

โปรแกรมเรียกใช้การทดสอบไม่ทราบเกี่ยวกับโครูทีน ดังนั้นเราจึงไม่สามารถทำให้การทดสอบนี้เป็นฟังก์ชันระงับได้ เราสามารถlaunchโครูทีนโดยใช้ CoroutineScope เช่น ใน ViewModel แต่การทดสอบต้องเรียกใช้โครูทีนจนเสร็จสมบูรณ์ก่อนจึงจะส่งคืนได้ เมื่อฟังก์ชันการทดสอบแสดงผล การทดสอบจะสิ้นสุดลง โครูทีนที่เริ่มต้นด้วย launch คือโค้ดแบบอะซิงโครนัส ซึ่งอาจเสร็จสมบูรณ์ในอนาคต ดังนั้นในการทดสอบโค้ดแบบไม่พร้อมกัน คุณจึงต้องมีวิธีบอกให้การทดสอบรอจนกว่าโครูทีนจะเสร็จสมบูรณ์ เนื่องจาก launch เป็นการเรียกที่ไม่บล็อก ซึ่งหมายความว่าฟังก์ชันจะแสดงผลทันทีและสามารถเรียกใช้โครูทีนต่อไปได้หลังจากที่ฟังก์ชันแสดงผลแล้ว จึงไม่สามารถใช้ในการทดสอบได้ เช่น

@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 เรียกฟังก์ชันระงับหรือ launches โครูทีนใหม่ ระบบจะดำเนินการทันทีโดยค่าเริ่มต้น คุณสามารถคิดว่านี่เป็นวิธีเปลี่ยนฟังก์ชันที่ระงับและโครูทีนให้เป็นการเรียกฟังก์ชันปกติ

นอกจากนี้ runBlockingTest จะส่งข้อยกเว้นที่ไม่ได้แคชซ้ำให้คุณ ซึ่งจะช่วยให้ทดสอบได้ง่ายขึ้นเมื่อโครูทีนส่งข้อยกเว้น

ใช้การทดสอบกับโครูทีน 1 รายการ

เรียกใช้ refreshTitle ด้วย runBlockingTest และนำ Wrapper GlobalScope.launch ออกจาก 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")
}

การทดสอบนี้ใช้ข้อมูลจำลองที่ระบุเพื่อตรวจสอบว่า refreshTitle แทรก "OK" ลงในฐานข้อมูลหรือไม่

เมื่อการทดสอบเรียกใช้ runBlockingTest ระบบจะบล็อกจนกว่าโครูทีนที่ runBlockingTest เริ่มต้นจะเสร็จสมบูรณ์ จากนั้นเมื่อเราเรียกใช้ refreshTitle ภายใน ระบบจะใช้กลไกการระงับและกลับมาทำงานต่อตามปกติเพื่อรอให้เพิ่มแถวฐานข้อมูลลงในฐานข้อมูลจำลอง

หลังจากที่โครูทีนการทดสอบเสร็จสมบูรณ์แล้ว runBlockingTest จะคืนค่า

เขียนการทดสอบการหมดเวลา

เราต้องการเพิ่มการหมดเวลาสั้นๆ ในคำขอเครือข่าย มาเขียนการทดสอบก่อน แล้วจึงใช้การหมดเวลา สร้างการทดสอบใหม่

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ปลอมที่ระบุ ซึ่งเป็นMainNetworkCompletableFakeปลอมของเครือข่ายที่ออกแบบมาเพื่อระงับผู้โทรจนกว่าการทดสอบจะดำเนินการต่อ เมื่อ refreshTitle พยายามส่งคำขอเครือข่าย ระบบจะหยุดทำงานตลอดไปเนื่องจากเราต้องการทดสอบการหมดเวลา

จากนั้นจะเปิดใช้โครูทีนแยกต่างหากเพื่อเรียกใช้ refreshTitle นี่เป็นส่วนสำคัญของการทดสอบการหมดเวลา การหมดเวลาควรเกิดขึ้นในโครูทีนอื่นที่ไม่ใช่โครูทีนที่ runBlockingTest สร้างขึ้น การทำเช่นนี้จะทำให้เราเรียกบรรทัดถัดไป advanceTimeBy(5_000) ซึ่งจะเลื่อนเวลาไป 5 วินาทีและทำให้โคโรทีนอื่นๆ หมดเวลา

นี่คือการทดสอบการหมดเวลาที่สมบูรณ์ และจะผ่านเมื่อเราใช้การหมดเวลา

เรียกใช้ตอนนี้แล้วดูว่าจะเกิดอะไรขึ้น

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

ฟีเจอร์อย่างหนึ่งของ runBlockingTest คือจะไม่ยอมให้คุณรั่วไหลโครูทีนหลังจากที่การทดสอบเสร็จสมบูรณ์ หากมีโครูทีนที่ยังไม่เสร็จ เช่น โครูทีนการเปิดตัวของเรา เมื่อสิ้นสุดการทดสอบ การทดสอบจะล้มเหลว

เพิ่มการหมดเวลา

เปิด TitleRepository แล้วเพิ่มการหมดเวลา 5 วินาทีในการดึงข้อมูลเครือข่าย ซึ่งทำได้โดยใช้ฟังก์ชัน 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)
   }
}

ทำการทดสอบ เมื่อทำการทดสอบ คุณควรเห็นว่าการทดสอบทั้งหมดผ่าน

ในแบบฝึกหัดถัดไป คุณจะได้เรียนรู้วิธีเขียนฟังก์ชันลำดับสูงโดยใช้โครูทีน

ในแบบฝึกหัดนี้ คุณจะปรับโครงสร้าง refreshTitle ใน MainViewModel เพื่อใช้ฟังก์ชันการโหลดข้อมูลทั่วไป ซึ่งจะสอนวิธีสร้างฟังก์ชันลำดับสูงที่ใช้โครูทีน

การใช้งาน refreshTitle ในปัจจุบันใช้งานได้ แต่เราสามารถสร้างโครูทีนการโหลดข้อมูลทั่วไปที่จะแสดงตัวหมุนเสมอ ซึ่งอาจเป็นประโยชน์ในโค้ดเบสที่โหลดข้อมูลเพื่อตอบสนองต่อเหตุการณ์หลายอย่าง และต้องการให้มั่นใจว่าระบบจะแสดงวงกลมโหลดอย่างสม่ำเสมอ

การตรวจสอบการติดตั้งใช้งานปัจจุบันทุกบรรทัดยกเว้น 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
       }
   }
}

การใช้โครูทีนในฟังก์ชันลำดับที่สูงกว่า

เพิ่มโค้ดนี้ลงใน 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
       }
   }
}

ตอนนี้ให้ปรับโครงสร้าง refreshTitle() เพื่อใช้ฟังก์ชันลำดับที่สูงกว่านี้

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

การแยกตรรกะเกี่ยวกับการแสดงวงกลมโหลดและข้อผิดพลาดช่วยให้เราลดความซับซ้อนของโค้ดจริงที่จำเป็นต่อการโหลดข้อมูล การแสดงวงล้อหมุนหรือการแสดงข้อผิดพลาดเป็นสิ่งที่สรุปได้ง่ายสำหรับการโหลดข้อมูลใดๆ ในขณะที่ต้องระบุแหล่งข้อมูลและปลายทางที่แท้จริงทุกครั้ง

หากต้องการสร้างการแยกข้อมูลนี้ launchDataLoad จะใช้อาร์กิวเมนต์ block ซึ่งเป็น lambda ที่ระงับ Suspend Lambda ช่วยให้คุณเรียกฟังก์ชัน Suspend ได้ ซึ่งเป็นวิธีที่ Kotlin ใช้สร้างโครูทีน launch และ runBlocking ที่เราใช้ใน Codelab นี้

// suspend lambda

block: suspend () -> Unit

หากต้องการสร้างฟังก์ชันแลมบ์ดาที่ระงับ ให้เริ่มต้นด้วยคีย์เวิร์ด suspend ลูกศรฟังก์ชันและประเภทการคืนค่า Unit จะทำให้การประกาศเสร็จสมบูรณ์

โดยปกติแล้วคุณไม่จำเป็นต้องประกาศแลมบ์ดาที่ระงับของคุณเอง แต่แลมบ์ดาเหล่านี้อาจมีประโยชน์ในการสร้างการแยกส่วนเช่นนี้ซึ่งห่อหุ้มตรรกะที่ซ้ำกัน

ในแบบฝึกหัดนี้ คุณจะได้เรียนรู้วิธีใช้โค้ดที่อิงตามโครูทีนจาก WorkManager

WorkManager คืออะไร

Android มีตัวเลือกมากมายสำหรับการทำงานในเบื้องหลังที่เลื่อนเวลาได้ แบบฝึกหัดนี้จะแสดงวิธีผสานรวม WorkManager กับโครูทีน WorkManager เป็นไลบรารีที่เข้ากันได้ ยืดหยุ่น และใช้งานง่ายสำหรับงานในเบื้องหลังที่เลื่อนเวลาได้ WorkManager เป็นโซลูชันที่แนะนำสำหรับกรณีการใช้งานเหล่านี้ใน Android

WorkManager เป็นส่วนหนึ่งของ Android Jetpack และเป็นคอมโพเนนต์สถาปัตยกรรมสำหรับงานในเบื้องหลังที่ต้องมีการดำเนินการแบบผสมผสานทั้งแบบมีโอกาสและแบบรับประกัน การดำเนินการแบบฉวยโอกาสหมายความว่า WorkManager จะทำงานในเบื้องหลังทันทีที่ทำได้ การดำเนินการที่รับประกันหมายความว่า WorkManager จะดูแลตรรกะในการเริ่มงานของคุณในสถานการณ์ต่างๆ แม้ว่าคุณจะออกจากแอปก็ตาม

ด้วยเหตุนี้ WorkManager จึงเป็นตัวเลือกที่ดีสำหรับงานที่ต้องดำเนินการให้เสร็จในที่สุด

ตัวอย่างงานที่ควรใช้ WorkManager มีดังนี้

  • การอัปโหลดบันทึก
  • การใช้ฟิลเตอร์กับรูปภาพและการบันทึกรูปภาพ
  • ซิงค์ข้อมูลในเครื่องกับเครือข่ายเป็นระยะๆ

การใช้โครูทีนกับ WorkManager

WorkManager มีการใช้งานListanableWorkerคลาสพื้นฐานที่แตกต่างกันสำหรับกรณีการใช้งานต่างๆ

คลาส Worker ที่ง่ายที่สุดช่วยให้เรามีการดำเนินการแบบซิงโครนัสบางอย่างที่ WorkManager ดำเนินการ อย่างไรก็ตาม จากการทำงานที่ผ่านมาเพื่อแปลงโค้ดเบสให้ใช้คอร์รูทีนและฟังก์ชันระงับ วิธีที่ดีที่สุดในการใช้ WorkManager คือผ่านคลาส CoroutineWorker ที่ช่วยให้เรากำหนดdoWork()ฟังก์ชันเป็นฟังก์ชันระงับได้

เปิด RefreshMainDataWork เพื่อเริ่มต้นใช้งาน ซึ่งขยาย CoroutineWorker อยู่แล้ว และคุณต้องติดตั้งใช้งาน doWork

ภายในฟังก์ชัน suspend doWork ให้เรียกใช้ 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 ที่เรียบง่ายกว่า โดยจะไม่ได้ทำงานใน Executor ที่ระบุในการกำหนดค่า WorkManager แต่จะใช้ Dispatcher ในสมาชิก coroutineContext (โดยค่าเริ่มต้นคือ Dispatchers.Default) แทน

การทดสอบ CoroutineWorker

ไม่มีฐานโค้ดใดที่สมบูรณ์ได้โดยไม่มีการทดสอบ

WorkManager มีวิธีต่างๆ 2-3 วิธีในการทดสอบ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 เพื่อสร้าง Worker ของเรา ซึ่งเราสามารถเรียกใช้โดยเรียกใช้เมธอด startWork()

WorkManager เป็นเพียงตัวอย่างหนึ่งของวิธีใช้โครูทีนเพื่อลดความซับซ้อนในการออกแบบ API

ใน Codelab นี้ เราได้กล่าวถึงพื้นฐานที่คุณจะต้องใช้เพื่อเริ่มใช้โครูทีนในแอป

เราได้พูดถึงหัวข้อต่อไปนี้

  • วิธีผสานรวมโครูทีนเข้ากับแอป Android จากทั้งงาน UI และ WorkManager เพื่อลดความซับซ้อนของการเขียนโปรแกรมแบบอะซิงโครนัส
  • วิธีใช้โครูทีนภายใน ViewModel เพื่อดึงข้อมูลจากเครือข่ายและบันทึกลงในฐานข้อมูลโดยไม่บล็อกเทรดหลัก
  • และวิธียกเลิกโครูทีนทั้งหมดเมื่อ ViewModel เสร็จสิ้น

สำหรับการทดสอบโค้ดที่ใช้โครูทีน เราได้ครอบคลุมทั้งการทดสอบลักษณะการทำงานและการเรียกใช้ฟังก์ชัน suspend จากการทดสอบโดยตรง

ดูข้อมูลเพิ่มเติม

ดู Codelab "Coroutines ขั้นสูงพร้อมด้วย Kotlin Flow และ LiveData" เพื่อดูการใช้งาน Coroutines ขั้นสูงเพิ่มเติมใน Android

โคโรทีนของ Kotlin มีฟีเจอร์มากมายที่ไม่ได้กล่าวถึงในโค้ดแล็บนี้ หากสนใจดูข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนของ Kotlin โปรดอ่านคำแนะนำเกี่ยวกับโครูทีนที่ JetBrains เผยแพร่ นอกจากนี้ โปรดดู "ปรับปรุงประสิทธิภาพของแอปด้วย Kotlin Coroutines" เพื่อดูรูปแบบการใช้งาน Coroutines ใน Android เพิ่มเติม