ใน 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 นี้:
... หรือโคลนที่เก็บ GitHub จากบรรทัดคําสั่งโดยใช้คําสั่งต่อไปนี้
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
คําถามที่พบบ่อย
อันดับแรก ลองมาดูตัวอย่างแอปเริ่มต้นกัน ทําตามวิธีการเหล่านี้เพื่อเปิดแอปตัวอย่างใน Android Studio
- หากดาวน์โหลดไฟล์ ZIP
kotlin-coroutines
ไฟล์ ให้แตกไฟล์ ZIP - เปิดโปรเจ็กต์
coroutines-codelab
ใน Android Studio - เลือกโมดูลแอปพลิเคชัน
start
- คลิกปุ่มเรียกใช้ แล้วเลือกโปรแกรมจําลองหรือเชื่อมต่ออุปกรณ์ Android ซึ่งต้องเรียกใช้ Android Lollipop ได้ (SDK ขั้นต่ําที่รองรับคือ 21) หน้าจอ Kotlin Coroutines ควรจะปรากฏขึ้นดังนี้
แอปเริ่มต้นนี้ใช้ชุดข้อความเพื่อเพิ่มจํานวนหน่วงเวลาสั้นๆ หลังจากที่กดหน้าจอ ระบบจะดึงข้อมูลชื่อใหม่จากเครือข่ายและแสดงบนหน้าจอ ลองใช้งานเลย โดยคุณจะเห็นจํานวนและข้อความที่เปลี่ยนแปลงหลังจากผ่านไประยะหนึ่ง ใน Codelab นี้ คุณจะแปลงแอปพลิเคชันนี้เพื่อใช้ Coroutine
แอปนี้ใช้คอมโพเนนต์สถาปัตยกรรมสถาปัตยกรรมเพื่อแยกโค้ด UI ใน MainActivity
ออกจากตรรกะแอปพลิเคชันใน MainViewModel
ลองใช้เวลาสักครู่เพื่อทําความคุ้นเคยกับโครงสร้างของโปรเจ็กต์
MainActivity
แสดง UI ลงทะเบียน Listener การคลิก และสามารถแสดงSnackbar
ส่งเหตุการณ์ไปยังMainViewModel
และอัปเดตหน้าจอโดยอิงตามLiveData
ในMainViewModel
MainViewModel
จะจัดการกับกิจกรรมในonMainViewClicked
และจะสื่อสารกับMainActivity
โดยใช้LiveData.
Executors
กําหนดBACKGROUND,
ซึ่งสามารถทําสิ่งต่างๆ บนชุดข้อความในเบื้องหลังได้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 วินาทีก่อนที่จะแสดงสแน็คบาร์ แต่ก็มีความแตกต่างที่สําคัญดังนี้
viewModelScope.
launch
จะเริ่มคอร์คอร์ทีในviewModelScope
ซึ่งหมายความว่าเมื่องานที่เราส่งให้กับviewModelScope
ถูกยกเลิก คอร์ทารีนทั้งหมดในงาน/ขอบเขตนี้จะถูกยกเลิก หากผู้ใช้ออกจากกิจกรรมก่อนdelay
กลับมา ระบบจะยกเลิกโครูทีนนี้โดยอัตโนมัติเมื่อมีการเรียกใช้onCleared
เมื่อมีการทําลายรูปแบบการดู- เนื่องจาก
viewModelScope
จะมีผู้ส่งช่วงเริ่มต้นของDispatchers.Main
ระบบจึงจะเปิดตัว Coroutine นี้ในชุดข้อความหลัก เราจะเห็นวิธีใช้ชุดข้อความที่แตกต่างกันภายหลัง - ฟังก์ชัน
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 ในการทดสอบนอกอุปกรณ์ได้
InstantTaskExecutorRule
คือกฎ JUnit ที่กําหนดค่าLiveData
เพื่อดําเนินการกับแต่ละงานพร้อมกัน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 วินาทีจึงจะตั้งค่าได้
ทําการทดสอบที่มีอยู่
- คลิกขวาที่ชื่อชั้นเรียน
MainViewModelTest
ในตัวแก้ไขเพื่อเปิดเมนูตามบริบท - ในเมนูตามบริบท ให้เลือกเรียกใช้ 'MainViewModelTest'
- สําหรับการดําเนินการในอนาคต คุณสามารถเลือกการกําหนดค่าการทดสอบนี้ในการกําหนดค่าถัดจากปุ่ม ในแถบเครื่องมือ โดยค่าเริ่มต้น การกําหนดค่าจะมีชื่อว่า MainViewModelTest
คุณควรเห็นบัตรผ่านสําหรับทดสอบ และใช้เวลาไม่ถึง 1 วินาทีในการเรียกใช้
ในแบบฝึกหัดถัดไป คุณจะได้ดูวิธีแปลงโค้ดเรียกกลับ API ที่มีอยู่เพื่อใช้โครูทีน
ในขั้นตอนนี้ คุณจะเริ่มแปลงที่เก็บเพื่อใช้ Coroutine ในการดําเนินการนี้ เราจะเพิ่มโครูทีนใน ViewModel
, Repository
, Room
และ Retrofit
การทราบว่าแต่ละส่วนของสถาปัตยกรรมมีหน้าที่รับผิดชอบอย่างไรก่อนที่เราจะเปลี่ยนไปใช้สไตล์คอร์ทารีน
MainDatabase
ใช้ฐานข้อมูลโดยใช้ Room ที่บันทึกและโหลดTitle
MainNetwork
จะใช้ API เครือข่ายที่ดึงข้อมูลชื่อใหม่ ใช้ Retrofit เพื่อดึงชื่อRetrofit
ได้รับการกําหนดค่าให้ส่งคืนข้อผิดพลาดหรือจําลองข้อมูลแบบสุ่ม แต่จะทํางานราวกับว่าเป็นการส่งคําขอเครือข่ายจริงTitleRepository
ใช้ API เดียวสําหรับการดึงข้อมูลหรือรีเฟรชชื่อด้วยการรวมข้อมูลจากเครือข่ายและฐานข้อมูล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
}
ทั้งนี้ ข้อยกเว้นในฟังก์ชันการระงับจะทํางานเช่นเดียวกับข้อผิดพลาดในฟังก์ชันปกติ หากคุณใส่ข้อผิดพลาดในฟังก์ชันการระงับ ระบบจะส่งข้อผิดพลาดไปยังผู้โทร ดังนั้น แม้ว่าระบบจะดําเนินการค่อนข้างแตกต่างกัน แต่คุณก็ใช้การบล็อกแบบทดลอง/บล็อกตามปกติเพื่อจัดการได้ วิธีนี้มีประโยชน์เนื่องจากต้องใช้การรองรับภาษาในตัวสําหรับการจัดการข้อผิดพลาดแทนการจัดการข้อผิดพลาดที่กําหนดเองสําหรับการติดต่อกลับทั้งหมด
และหากคุณโยนข้อยกเว้นออกจากโครูทีน โครูทีนดังกล่าวจะยกเลิกผู้ปกครองโดยค่าเริ่มต้น นั่นหมายความว่าการยกเลิกงานที่เกี่ยวข้องหลายรายการพร้อมกันสามารถทําได้อย่างง่ายดาย
จากนั้น ในบล็อกสุดท้าย เราก็มั่นใจได้ว่าจะปิดสปินเนอร์ไว้เสมอหลังจากการค้นหาทํางาน
เรียกใช้แอปพลิเคชันอีกครั้งโดยเลือกการกําหนดค่าเริ่มต้น แล้วกด คุณจะเห็นไอคอนกําลังโหลดเมื่อแตะที่ใดก็ได้ ชื่อจะยังคงเหมือนเดิมเนื่องจากเรายังไม่ได้เชื่อมต่อเครือข่ายหรือฐานข้อมูลของเรา
ในการออกกําลังกายครั้งถัดไป คุณจะอัปเดตที่เก็บข้อมูลเพื่อทํางานจริงๆ
ในแบบฝึกหัดนี้ คุณจะได้ทราบวิธีเปลี่ยนชุดข้อความที่ 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
กับโค้ดเรียกกลับเพื่อสื่อสารสถานะการโหลดและข้อผิดพลาดกับผู้โทร
ฟังก์ชันการทํางานนี้จะดําเนินการบางอย่างเพื่อใช้การรีเฟรช
- เปลี่ยนเป็นชุดข้อความอื่นที่มี
BACKGROUND
ExecutorService
- เรียกใช้คําขอเครือข่าย
fetchNextTitle
โดยใช้วิธีการบล็อกexecute()
การดําเนินการนี้จะเรียกใช้คําขอเครือข่ายในชุดข้อความปัจจุบัน ซึ่งในกรณีนี้หนึ่งในชุดข้อความในBACKGROUND
- หากผลลัพธ์สําเร็จ ให้บันทึกไปยังฐานข้อมูลด้วย
insertTitle
และเรียกเมธอดonCompleted()
- หากผลลัพธ์ไม่สําเร็จหรือมีข้อยกเว้น ให้เรียกใช้เมธอด 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 ประการเมื่อเทียบกับเวอร์ชันที่ติดต่อกลับ ดังนี้
withContext
จะส่งคืนผลการค้นหากลับไปยังผู้จัดจําหน่ายที่ระบุชื่อไว้ ซึ่งในกรณีนี้คือDispatchers.Main
เวอร์ชันโค้ดเรียกกลับเรียกโค้ดเรียกกลับของชุดข้อความในบริการปฏิบัติการBACKGROUND
- ผู้โทรไม่จําเป็นต้องโทรกลับไปยังฟังก์ชันนี้ ผู้ใช้ระงับและกลับมาเล่นต่อเพื่อรับผลลัพธ์หรือข้อผิดพลาดได้
เรียกใช้แอปอีกครั้ง
หากเรียกใช้แอปอีกครั้ง คุณจะเห็นว่าการใช้งาน 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 อย่างต่อไปนี้
- เพิ่มตัวแก้ไขการระงับลงในฟังก์ชัน
- นํา 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
- กด Alt-Enter เพิ่มตัวแก้ไขการระงับไปยังฟังก์ชันทั้งหมดในฟีเจอร์มากมาย
หน้าหลักเครือข่าย
- กด Alt-Enter เพิ่มตัวแก้ไขการระงับไปยังฟังก์ชันทั้งหมดในฟีเจอร์มากมาย
- แทนที่
fetchNextTitle
ด้วยฟังก์ชันนี้
override suspend fun fetchNextTitle() = result
หน้าหลักเครือข่ายสมบูรณ์แบบ
- กด Alt-Enter เพิ่มตัวแก้ไขการระงับไปยังฟังก์ชันทั้งหมดในฟีเจอร์มากมาย
- แทนที่
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
' เครื่องจัดการข้อยกเว้นที่ไม่พบแทน
ไลบรารี 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