ใน 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 นี้
... หรือโคลนที่เก็บ GitHub จากบรรทัดคำสั่งโดยใช้คำสั่งต่อไปนี้
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
คำถามที่พบบ่อย
ก่อนอื่น มาดูแอปตัวอย่างเริ่มต้นกัน ทำตามวิธีการเหล่านี้เพื่อเปิดแอปตัวอย่างใน Android Studio
- หากดาวน์โหลดไฟล์ ZIP
kotlin-coroutines
ให้แตกไฟล์ - เปิด
coroutines-codelab
โปรเจ็กต์ใน Android Studio - เลือกโมดูลแอปพลิเคชัน
start
- คลิกปุ่ม
เรียกใช้ แล้วเลือกโปรแกรมจำลองหรือเชื่อมต่ออุปกรณ์ Android ซึ่งต้องสามารถเรียกใช้ Android Lollipop ได้ (SDK ขั้นต่ำที่รองรับคือ 21) หน้าจอ Kotlin Coroutines ควรปรากฏขึ้น
แอปเริ่มต้นนี้ใช้เธรดเพื่อเพิ่มจำนวนโดยมีการหน่วงเวลาสั้นๆ หลังจากที่คุณกดหน้าจอ นอกจากนี้ยังจะดึงชื่อใหม่จากเครือข่ายและแสดงบนหน้าจอด้วย ลองเลยตอนนี้ แล้วคุณจะเห็นว่าจำนวนและข้อความเปลี่ยนไปหลังจากผ่านไปครู่หนึ่ง ใน Codelab นี้ คุณจะแปลงแอปพลิเคชันนี้ให้ใช้โคโรทีน
แอปนี้ใช้คอมโพเนนต์สถาปัตยกรรมเพื่อแยกโค้ด UI ใน MainActivity
ออกจากตรรกะของแอปพลิเคชันใน MainViewModel
โปรดสละเวลาสักครู่เพื่อทำความคุ้นเคยกับโครงสร้างของโปรเจ็กต์
MainActivity
แสดง UI ลงทะเบียน Listener การคลิก และแสดงSnackbar
ได้ โดยจะส่งเหตุการณ์ไปยังMainViewModel
และอัปเดตหน้าจอตามLiveData
ในMainViewModel
MainViewModel
จัดการเหตุการณ์ในonMainViewClicked
และจะสื่อสารกับMainActivity
โดยใช้LiveData.
Executors
definesBACKGROUND,
ซึ่งสามารถเรียกใช้สิ่งต่างๆ ในเธรดเบื้องหลังได้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 แต่มีความแตกต่างที่สำคัญบางประการดังนี้
viewModelScope.
launch
จะเริ่มโครูทีนในviewModelScope
ซึ่งหมายความว่าเมื่อมีการยกเลิกงานที่เราส่งไปยังviewModelScope
ระบบจะยกเลิกโครูทีนทั้งหมดในงาน/ขอบเขตนี้ หากผู้ใช้ออกจากกิจกรรมก่อนที่delay
จะแสดงผล ระบบจะยกเลิกโครูทีนนี้โดยอัตโนมัติเมื่อมีการเรียกonCleared
เมื่อ ViewModel ถูกทำลาย- เนื่องจาก
viewModelScope
มีตัวจัดส่งเริ่มต้นเป็นDispatchers.Main
ดังนั้นระบบจะเปิดใช้โครูทีนนี้ในเทรดหลัก เราจะมาดูวิธีใช้เธรดต่างๆ ในภายหลัง - ฟังก์ชัน
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 ในการทดสอบนอกอุปกรณ์ได้ ดังนี้
InstantTaskExecutorRule
เป็นกฎ JUnit ที่กำหนดค่าLiveData
ให้เรียกใช้งานแต่ละงานแบบพร้อมกัน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 วินาทีเพื่อให้ตั้งค่า
เรียกใช้การทดสอบที่มีอยู่
- คลิกขวาที่ชื่อคลาส
MainViewModelTest
ในโปรแกรมแก้ไขเพื่อเปิดเมนูตามบริบท - ในเมนูตามบริบท ให้เลือก
Run 'MainViewModelTest'
- สำหรับการทดสอบในอนาคต คุณสามารถเลือกการกำหนดค่าการทดสอบนี้ในการกำหนดค่าข้างปุ่ม
ในแถบเครื่องมือ โดยค่าเริ่มต้น ระบบจะเรียกการกำหนดค่าว่า MainViewModelTest
คุณควรเห็นว่าการทดสอบผ่าน และควรใช้เวลาไม่ถึง 1 วินาทีในการเรียกใช้
ในแบบฝึกหัดถัดไป คุณจะได้เรียนรู้วิธีแปลงจาก API การเรียกกลับที่มีอยู่เพื่อใช้โครูทีน
ในขั้นตอนนี้ คุณจะเริ่มแปลงที่เก็บเพื่อใช้โคโรทีน เราจะเพิ่มโครูทีนลงใน ViewModel
, Repository
, Room
และ Retrofit
เพื่อดำเนินการนี้
คุณควรทำความเข้าใจหน้าที่ของแต่ละส่วนในสถาปัตยกรรมก่อนที่เราจะเปลี่ยนไปใช้โครูทีน
MainDatabase
ใช้ฐานข้อมูลโดยใช้ Room ที่บันทึกและโหลดTitle
MainNetwork
ใช้ API เครือข่ายที่ดึงข้อมูลชื่อใหม่ โดยใช้ Retrofit เพื่อดึงข้อมูลชื่อRetrofit
ได้รับการกำหนดค่าให้แสดงข้อผิดพลาดหรือข้อมูลจำลองแบบสุ่ม แต่จะทำงานเหมือนกับว่ากำลังส่งคำขอเครือข่ายจริงTitleRepository
ใช้ API เดียวสําหรับการดึงหรือรีเฟรชชื่อโดยการรวมข้อมูลจากเครือข่ายและฐานข้อมูลMainViewModel
แสดงสถานะของหน้าจอและจัดการเหตุการณ์ ซึ่งจะบอกให้ที่เก็บรีเฟรชชื่อเมื่อผู้ใช้แตะหน้าจอ
เนื่องจากคำขอเครือข่ายเกิดจากเหตุการณ์ UI และเราต้องการเริ่มโครูทีนตามเหตุการณ์เหล่านั้น ViewModel
จึงเป็นที่ที่เหมาะสมในการเริ่มใช้โครูทีน
เวอร์ชันการโทรกลับ
เปิด MainViewModel.kt
เพื่อดูประกาศของ refreshTitle
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
ฟังก์ชันนี้จะเรียกใช้ทุกครั้งที่ผู้ใช้คลิกบนหน้าจอ และจะทำให้ที่เก็บรีเฟรชชื่อและเขียนชื่อใหม่ลงในฐานข้อมูล
การติดตั้งใช้งานนี้ใช้การเรียกกลับเพื่อทำสิ่งต่อไปนี้
- ก่อนที่จะเริ่มการค้นหา ระบบจะแสดงไอคอนหมุนของการโหลดพร้อมข้อความ
_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 เราจะตรวจสอบว่าได้ปิดสปินเนอร์ทุกครั้งหลังจากเรียกใช้การค้นหา
เรียกใช้แอปพลิเคชันอีกครั้งโดยเลือกการกำหนดค่าเริ่ม แล้วกด คุณควรเห็นวงกลมโหลดเมื่อแตะที่ใดก็ได้ ชื่อจะยังคงเหมือนเดิมเนื่องจากเรายังไม่ได้เชื่อมต่อเครือข่ายหรือฐานข้อมูล
ในแบบฝึกหัดถัดไป คุณจะอัปเดตรีพอสิทอรีเพื่อทำงานจริง
ในแบบฝึกหัดนี้ คุณจะได้เรียนรู้วิธีเปลี่ยนเธรดที่โครูทีนทำงานเพื่อใช้เวอร์ชันที่ใช้งานได้ของ 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
จะได้รับการติดตั้งใช้งานด้วยการเรียกกลับเพื่อสื่อสารสถานะการโหลดและข้อผิดพลาดกับผู้เรียก
ฟังก์ชันนี้จะทำหลายอย่างเพื่อใช้การรีเฟรช
- เปลี่ยนไปใช้ชุดข้อความอื่นด้วย
BACKGROUND
ExecutorService
- เรียกใช้
fetchNextTitle
คำขอเครือข่ายโดยใช้วิธีการบล็อกexecute()
ซึ่งจะเรียกใช้คำขอเครือข่ายในเธรดปัจจุบัน ในกรณีนี้คือเธรดใดเธรดหนึ่งในBACKGROUND
- หากผลลัพธ์สำเร็จ ให้บันทึกลงในฐานข้อมูลด้วย
insertTitle
และเรียกใช้เมธอดonCompleted()
- หากผลลัพธ์ไม่สำเร็จหรือมีข้อยกเว้น ให้เรียกใช้เมธอด 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 ประการดังนี้
withContext
จะส่งผลลัพธ์กลับไปยัง Dispatcher ที่เรียกใช้ ซึ่งในกรณีนี้คือDispatchers.Main
เวอร์ชันการเรียกกลับจะเรียกการเรียกกลับในเธรดในBACKGROUND
บริการตัวดำเนินการ- ผู้โทรไม่จำเป็นต้องส่ง 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 สิ่งต่อไปนี้
- เพิ่มตัวแก้ไขระงับลงในฟังก์ชัน
- นำ 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
- กด Alt+Enter เพื่อเพิ่มตัวแก้ไขระงับไปยังฟังก์ชันทั้งหมดในลำดับชั้น
MainNetworkFake
- กด Alt+Enter เพื่อเพิ่มตัวแก้ไขระงับไปยังฟังก์ชันทั้งหมดในลำดับชั้น
- แทนที่
fetchNextTitle
ด้วยฟังก์ชันนี้
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- กด Alt+Enter เพื่อเพิ่มตัวแก้ไขระงับไปยังฟังก์ชันทั้งหมดในลำดับชั้น
- แทนที่
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 เพิ่มเติม