ใช้ Kotlin Coroutines ในแอป Android

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

นี่คือข้อมูลโค้ดที่จะช่วยให้คุณเห็นภาพคร่าวๆ ว่าคุณกําลังทําอะไร

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

ระบบจะแปลงโค้ดตามโค้ดเรียกกลับเป็นโค้ดตามลําดับโดยใช้ Coroutine

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

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

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

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

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

สิ่งที่คุณจะทํา

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

สิ่งที่ต้องมี

  • Android Studio 35 (Codelab อาจใช้งานได้กับเวอร์ชันอื่นๆ แต่อาจมีบางเวอร์ชันหรือมีลักษณะแตกต่างออกไป)

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

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

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

ดาวน์โหลด Zip

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

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

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

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

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

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

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

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

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

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

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

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

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

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 ขนาดใหญ่ เขียนข้อมูลลงในฐานข้อมูล หรือดึงข้อมูลจากเครือข่าย ดังนั้นการเรียกใช้โค้ดลักษณะนี้จากชุดข้อความหลักอาจทําให้แอปหยุดชั่วคราว กระตุก หรือแม้กระทั่งค้าง และหากคุณบล็อกชุดข้อความหลักนานเกินไป แอปอาจขัดข้องและแสดงข้อความแอปพลิเคชันไม่ตอบสนอง

ดูวิดีโอแนะนําด้านล่างเกี่ยวกับวิธีที่ Coroutine แก้ไขปัญหาของเราบน 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) ช่วยให้ slowFetch ทํางานในเทรดพื้นหลังและแสดงผลการค้นหาเมื่อพร้อมได้

การใช้โครูทีนในการนําโค้ดเรียกกลับออก

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

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

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

คีย์เวิร์ด suspend คือวิธีทําเครื่องหมายฟังก์ชันหรือประเภทฟังก์ชันที่ใช้ได้ด้วย Cortine เมื่อ Coroutine เรียกใช้ฟังก์ชันที่ทําเครื่องหมาย 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 ทํางานได้อย่างยอดเยี่ยม

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

// 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 { ... }

คุณจะได้ลองใช้ Coroutine กับแอปตัวอย่างในหัวข้อถัดไป

ในแบบฝึกหัดนี้ คุณจะต้องเขียน Coroutine เพื่อแสดงข้อความหลังจากหน่วงเวลา หากต้องการเริ่มต้นใช้งาน ให้ตรวจสอบว่าคุณเปิดโมดูล start ใน Android Studio แล้ว

ทําความเข้าใจ CoroutineScope

Cortine ทั้งหมดจะทํางานใน CoroutineScope ขอบเขตนั้นควบคุมอายุการใช้งานของโครูทีนผ่านงาน เมื่อคุณยกเลิกงานของขอบเขต การดําเนินการนี้จะยกเลิก Coroutine ทั้งหมดที่เริ่มต้นในขอบเขตนั้น ใน Android คุณสามารถใช้ขอบเขตยกเลิก Coroutine ทั้งหมดที่ทํางานอยู่ได้ เช่น เมื่อผู้ใช้ออกจาก Activity หรือ Fragment ขอบเขตยังช่วยให้คุณระบุผู้ส่งเริ่มต้นได้ด้วย ผู้จัดการจะควบคุมชุดข้อความที่จะเรียกใช้ Coroutine

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

การใช้ viewModelScope

ไลบรารี AndroidX lifecycle-viewmodel-ktx เพิ่ม CoroutineScope ใน ViewModels ที่กําหนดค่าให้เริ่มต้น Coroutine ที่เกี่ยวข้องกับ UI หากต้องการใช้ไลบรารีนี้ คุณต้องระบุไลบรารีนี้ในไฟล์ build.gradle (Module: start) ของโปรเจ็กต์ ขั้นตอนดังกล่าวจะทําในโปรเจ็กต์ Codelab เรียบร้อยแล้ว

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

ไลบรารีเพิ่ม viewModelScope เป็นฟังก์ชันส่วนขยายของคลาส ViewModel ขอบเขตนี้ผูกกับ Dispatchers.Main และจะถูกยกเลิกโดยอัตโนมัติเมื่อล้าง ViewModel

เปลี่ยนจากชุดข้อความเป็น Coroutine

ใน 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 ออกจากโค้ดแล้วเรียกใช้อีกครั้ง ไอคอนหมุนจะไม่แสดงขึ้น และระบบจะโหลดทุกอย่างเป็น "jump" เป็นสถานะสุดท้ายในอีก 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 ด้วยโค้ด Coroutine นี้ที่ทําแบบเดียวกัน คุณจะต้องนําเข้า 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 วินาทีก่อนที่จะแสดงสแน็คบาร์ แต่ก็มีความแตกต่างที่สําคัญดังนี้

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

เริ่มเลย เมื่อคุณคลิกมุมมองหลัก คุณจะเห็น Snackbar ในภายหลัง

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

ในแบบฝึกหัดนี้ คุณจะต้องเขียนการทดสอบสําหรับโค้ดที่เพิ่งเขียนไว้ แบบฝึกหัดนี้แสดงวิธีทดสอบโครูทีนที่ทํางานอยู่ใน Dispatchers.Main โดยใช้ไลบรารี kotlinx-coroutines-test โดยคุณจะใช้การทดสอบที่โต้ตอบกับ Coroutine ได้โดยตรงภายหลังใน Codelab นี้

ตรวจสอบโค้ดที่มีอยู่

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

สําหรับการทดสอบนี้ ไฟล์ปลอมที่จําเป็นเพื่อปฏิบัติตามทรัพยากร Dependency ของ MainViewModel เท่านั้น ในภายหลัง ห้องทดลองของโค้ดนี้จะให้คุณอัปเดตปลอมเพื่อรองรับ Coroutine

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

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

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

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

ทําการทดสอบที่มีอยู่

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

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

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

ในขั้นตอนนี้ คุณจะเริ่มแปลงที่เก็บเพื่อใช้ Coroutine ในการดําเนินการนี้ เราจะเพิ่มโครูทีนใน ViewModel, Repository, Room และ Retrofit

การทราบว่าแต่ละส่วนของสถาปัตยกรรมมีหน้าที่รับผิดชอบอย่างไรก่อนที่เราจะเปลี่ยนไปใช้สไตล์คอร์ทารีน

  1. MainDatabase ใช้ฐานข้อมูลโดยใช้ Room ที่บันทึกและโหลด Title
  2. MainNetwork จะใช้ API เครือข่ายที่ดึงข้อมูลชื่อใหม่ ใช้ Retrofit เพื่อดึงชื่อ Retrofit ได้รับการกําหนดค่าให้ส่งคืนข้อผิดพลาดหรือจําลองข้อมูลแบบสุ่ม แต่จะทํางานราวกับว่าเป็นการส่งคําขอเครือข่ายจริง
  3. TitleRepository ใช้ API เดียวสําหรับการดึงข้อมูลหรือรีเฟรชชื่อด้วยการรวมข้อมูลจากเครือข่ายและฐานข้อมูล
  4. MainViewModel แสดงถึงสถานะหน้าจอและจัดการเหตุการณ์ อุปกรณ์จะบอกให้ที่เก็บรีเฟรชชื่อเมื่อผู้ใช้แตะหน้าจอ

เนื่องจากคําขอเครือข่ายเกิดจากเหตุการณ์ UI และเราต้องการเริ่ม Coroutine โดยพิจารณาจากเหตุการณ์ดังกล่าว สถานที่ธรรมชาติสําหรับการเริ่มใช้ Coroutine จึงอยู่ใน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 แสดงและล้างไอคอนหมุน

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

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

เวอร์ชัน Coroutine

มาเขียน refreshTitle ด้วย Coroutine ใหม่กัน

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

TitleRepository.kt

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

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

ใน MainViewModel ให้แทนที่ refreshTitle เวอร์ชันเรียกกลับด้วยการเปิดตัว Coroutine ใหม่ดังนี้

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 {

เช่นเดียวกับ Coroutine ในการอัปเดตจํานวนการแตะ ให้เริ่มต้นด้วยการเปิด Coroutine ใหม่ใน viewModelScope การดําเนินการนี้จะใช้ Dispatchers.Main ซึ่งไม่เป็นไร แม้ว่า refreshTitle จะส่งคําขอเครือข่ายและการค้นหาในฐานข้อมูล แต่ก็สามารถใช้ Coroutine เพื่อแสดงอินเทอร์เฟซที่เหมาะสําหรับความปลอดภัยได้ ซึ่งหมายความว่าคุณจะเรียกใช้ชุดข้อความดังกล่าวจากชุดข้อความหลักได้อย่างปลอดภัย

เนื่องจากเราจะใช้ viewModelScope เมื่อผู้ใช้ออกจากหน้าจอนี้ งานที่เริ่มโดย Coroutine นี้จะถูกยกเลิกโดยอัตโนมัติ ซึ่งหมายความว่าระบบจะไม่ส่งคําขอเครือข่ายหรือคําค้นหาฐานข้อมูลเพิ่มเติม

โค้ดเพียงไม่กี่บรรทัดเท่านั้นจะเรียกใช้ refreshTitle ใน repository

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

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

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

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

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

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

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

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

ในการออกกําลังกายครั้งถัดไป คุณจะอัปเดตที่เก็บข้อมูลเพื่อทํางานจริงๆ

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

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

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

ชื่อ Repository.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 จากภายในโครูทีน เช่น การจัดเรียงและกรองรายการขนาดใหญ่หรือการอ่านจากดิสก์

Coroutine จะใช้ withContext เพื่อสลับระหว่างแอปอื่น การโทรหา withContext จะเปลี่ยนมาใช้ผู้มอบหมายงานคนอื่นสําหรับแลมบ์ดาเท่านั้น แล้วกลับมาที่ผู้มอบหมายงานที่เรียกด้วยแลมบ์ดาดังกล่าว

โดยค่าเริ่มต้น Kotlin Coroutine จะแจกจ่ายเครื่องจ่าย 3 เครื่อง ได้แก่ Main, IO และ Default ผู้มอบหมายงาน IO ได้รับการเพิ่มประสิทธิภาพให้เหมาะกับงาน IO เช่น การอ่านจากเครือข่ายหรือดิสก์ ส่วน TCF เริ่มต้นจะได้รับการเพิ่มประสิทธิภาพสําหรับงานที่ต้องใช้ CPU

ชื่อ Repository.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 จะเสร็จสิ้น

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

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

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

หากเรียกใช้แอปอีกครั้ง คุณจะเห็นว่าการใช้งาน Coroutine ใหม่กําลังโหลดผลการค้นหาจากเครือข่าย

ในขั้นตอนถัดไป คุณจะผสานรวมโครูทีนไว้ในห้องและฟื้นฟู

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

Coutoutine ในห้อง

เปิด MainDatabase.kt ครั้งแรกและทําให้ insertTitle เป็นฟังก์ชันระงับ

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

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

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

เพียงเท่านี้ก็ใช้คอร์ทารีนในห้องได้แล้ว สุดยอด

Coroutine ใน Retrofit

ต่อไปมาดูวิธีผสานรวม Coroutine กับ 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 เวอร์ชันปรับปรุงเต็มรูปแบบ คุณจะส่งคืน Result<String> จาก String จากฟังก์ชันการระงับได้

การเพิกถอนจะทําให้ฟังก์ชันการระงับปลอดภัยโดยอัตโนมัติ คุณจึงเรียกใช้ฟังก์ชันดังกล่าวได้โดยตรงจาก Dispatchers.Main

การใช้ห้องและการปรับห้อง

ตอนนี้เมื่อ 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

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

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

ข้ามโปรเจ็กต์และแก้ไขข้อผิดพลาดของคอมไพเลอร์โดยเปลี่ยนฟังก์ชันให้ระงับการสร้าง ความละเอียดอย่างรวดเร็วของแต่ละภาพมีดังนี้

TestingFakes.kt

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

ชื่อDaoFake

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

หน้าหลักเครือข่าย

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

หน้าหลักเครือข่ายสมบูรณ์แบบ

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

ชื่อ Repository.kt

  • ลบฟังก์ชัน refreshTitleWithCallbacks เนื่องจากไม่มีการใช้งานอีกต่อไป

เรียกใช้แอป

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

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

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

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

ฟังก์ชัน refreshTitle ที่คุณใส่ไว้ในการออกกําลังกายครั้งล่าสุดมีดังนี้

ชื่อ 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)
   }
}

เขียนการทดสอบโดยเรียกฟังก์ชันการระงับ

เปิด TitleRepositoryTest.kt ในโฟลเดอร์ test ที่มี TODOS 2 รายการ

ลองเรียก refreshTitle จากการทดสอบครั้งแรก whenRefreshTitleSuccess_insertsRows

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

   subject.refreshTitle()
}

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

ผู้เรียกใช้การทดสอบไม่ทราบอะไรเกี่ยวกับ Coroutine เลย เราจึงไม่สามารถทําให้การทดสอบนี้เป็นฟังก์ชันระงับ เราอาจใช้ launch โครูทีนโดยใช้ CoroutineScope เช่น ViewModel ก็ได้ แต่การทดสอบต้องเรียกใช้คอร์คอร์ตินจนเสร็จก่อนที่จะกลับมา เมื่อฟังก์ชันทดสอบส่งกลับ การทดสอบก็จะจบลง โครูทีนที่ขึ้นต้นด้วย launch เป็นโค้ดแบบอะซิงโครนัสซึ่งอาจเสร็จสมบูรณ์ในอนาคต ดังนั้น หากต้องการทดสอบโค้ดแบบอะซิงโครนัส คุณต้องมีวิธีบอกการทดสอบให้รอจนกว่าโครูทีนจะเสร็จสมบูรณ์ เนื่องจาก 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&#39 เครื่องจัดการข้อยกเว้นที่ไม่พบแทน

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

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

ใช้การทดสอบกับ Coroutine 1 รายการ

ตัดการเรียก refreshTitle กับ runBlockingTest และนํา GlobalScope.launch Wrapper ออกจาก subject.refreshTitle()

ชื่อRepositoryTest.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 ก็จะใช้กลไกการระงับและกลับมาใช้งานเป็นประจําเพื่อรอให้ระบบเพิ่มแถวฐานข้อมูลลงในปลอม

หลังจากเสร็จสิ้นการทดสอบแล้ว runBlockingTest จะส่งคืน

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

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

ชื่อRepositoryTest.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 นี่คือส่วนสําคัญของระยะหมดเวลาการทดสอบ ระยะหมดเวลาควรเกิดขึ้นในกรอบเวลา Correine ที่ต่างจากที่ runBlockingTest สร้าง การทําเช่นนี้จะทําให้เราโทรหาบรรทัดถัดไปคือ advanceTimeBy(5_000) ซึ่งจะเลื่อนเวลาออกไปอีก 5 วินาทีและทําให้ Coroutine หมดเวลาอีกครั้ง

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

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

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

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

เพิ่มระยะหมดเวลา

เปิด TitleRepository และเพิ่มระยะหมดเวลา 5 วินาทีลงในการดึงข้อมูลเครือข่าย โดยใช้ฟังก์ชัน withTimeout ดังนี้

ชื่อ Repository.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 เพื่อใช้ฟังก์ชันการโหลดข้อมูลทั่วไป การดําเนินการนี้จะสอนให้คุณสร้างฟังก์ชันลําดับที่สูงขึ้นโดยใช้ Coroutine

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

การตรวจสอบการติดตั้งใช้งานปัจจุบันทุกบรรทัด ยกเว้น repository.refreshTitle() คือ Boilerplate เพื่อแสดงข้อผิดพลาดของไอคอนหมุนและดิสเพลย์

// 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 ซึ่งเป็นแลมบ์ดาที่ถูกระงับ แลมบ์ดาที่ถูกระงับจะช่วยให้คุณเรียกใช้ฟังก์ชันการระงับได้ นั่นหมายถึง Kotlin นําเครื่องมือสร้าง Coroutine launch และ runBlocking ที่เราใช้ใน Codelab นี้ไปใช้

// suspend lambda

block: suspend () -> Unit

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

คุณไม่จําเป็นต้องประกาศแลมบ์ดา้ส์เองบ่อยครั้ง แต่ก็เพราะมันจะช่วยสร้างนามธรรมแบบนี้ที่สรุปตรรกะซ้ําๆ

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

WorkManager คืออะไร

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

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

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

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

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

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

WorkManager จะติดตั้งใช้งาน ListanableWorker คลาสพื้นฐานสําหรับกรณีการใช้งานที่แตกต่างกัน

คลาส Worker ที่เรียบง่ายที่สุดช่วยให้เราดําเนินการบางอย่างด้วย WorkManager ได้ อย่างไรก็ตาม ที่ผ่านมาเราทําการแปลงฐานของโค้ดเพื่อใช้ Coroutine และระงับฟังก์ชัน วิธีที่ดีที่สุดในการใช้ 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() เป็นฟังก์ชันการระงับ โค้ดนี้จะไม่ทํางานใน Executer ที่ระบุในการกําหนดค่า WorkManager แต่จะใช้ผู้มอบหมายงานในสมาชิก coroutineContext แทน (โดยค่าเริ่มต้นคือ Dispatchers.Default) ซึ่งต่างจากคลาส Worker ที่เรียบง่ายขึ้น

การทดสอบ CoroutineWorker ของเรา

ไม่ควรมีฐานของโค้ดที่ครบถ้วนสมบูรณ์หากไม่มีการทดสอบ

WorkManager มีวิธีทดสอบคลาส Worker อยู่ 2 วิธี หากต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับโครงสร้างพื้นฐานในการทดสอบเดิม โปรดอ่านเอกสารประกอบ

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 เป็นเพียงตัวอย่างหนึ่งเกี่ยวกับวิธีใช้ Coroutine เพื่อทําให้การออกแบบ API ง่ายขึ้นได้

ใน Codelab นี้ เราได้พูดถึงข้อมูลเบื้องต้นที่คุณจะต้องใช้งาน Coroutine ในแอปของคุณ

เราพูดถึงสิ่งเหล่านี้

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

สําหรับการทดสอบโค้ดแบบ Coroutine เราจะพูดถึงทั้ง 2 ลักษณะการทํางานด้วยการทดสอบและการเรียกฟังก์ชัน suspend จากการทดสอบโดยตรง

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

ดู "Cooutine ขั้นสูงด้วย Kotlin Flow และ LiveData" codelab เพื่อดูการใช้งาน Coroutine ขั้นสูงบน Android

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