โครูทีนคือรูปแบบการออกแบบการทำงานพร้อมกันที่คุณใช้ใน Android เพื่อลดความซับซ้อนของโค้ดที่ทำงานแบบไม่พร้อมกันได้ โครูทีน ได้รับการเพิ่มลงใน Kotlin ในเวอร์ชัน 1.3 และอิงตามแนวคิดที่ได้รับการยอมรับ จากภาษาอื่นๆ
ใน Android คอรทีนจะช่วยจัดการงานที่ใช้เวลานานซึ่งอาจบล็อกเทรดหลักและทำให้แอปไม่ตอบสนอง นักพัฒนาซอฟต์แวร์มืออาชีพกว่า 50% ที่ใช้ Coroutine รายงานว่ามีประสิทธิภาพการทำงานเพิ่มขึ้น หัวข้อนี้อธิบายวิธีใช้โครูทีน Kotlin เพื่อแก้ปัญหาเหล่านี้ ซึ่งจะช่วยให้คุณเขียนโค้ดแอปได้สะอาดและกระชับยิ่งขึ้น
ฟีเจอร์
Coroutines เป็นโซลูชันที่แนะนำสำหรับการเขียนโปรแกรมแบบอะซิงโครนัสใน Android ฟีเจอร์ที่น่าสนใจมีดังนี้
- น้ำหนักเบา: คุณเรียกใช้โครูทีนหลายรายการในเธรดเดียวได้เนื่องจาก รองรับ การระงับ ซึ่งจะไม่บล็อกเธรดที่โครูทีนกำลังทำงานอยู่ การระงับ ช่วยประหยัดหน่วยความจำมากกว่าการบล็อกในขณะที่รองรับการดำเนินการพร้อมกันจำนวนมาก
- หน่วยความจำรั่วไหลน้อยลง: ใช้ การทำงานพร้อมกันที่มีโครงสร้าง เพื่อเรียกใช้การดำเนินการภายในขอบเขต
- การรองรับการยกเลิกในตัว การยกเลิก จะเผยแพร่โดยอัตโนมัติผ่านลำดับชั้นของโครูทีนที่ทำงานอยู่
- การผสานรวม Jetpack: ไลบรารี Jetpack หลายรายการมีส่วนขยายที่รองรับ Coroutine อย่างเต็มรูปแบบ ไลบรารีบางรายการยังมีขอบเขตของโครูทีนของตัวเองที่คุณใช้เพื่อการทำงานพร้อมกันที่มีโครงสร้างได้
ภาพรวมตัวอย่าง
ตามคู่มือสถาปัตยกรรมแอป ตัวอย่างในหัวข้อนี้จะส่งคำขอเครือข่ายและแสดงผลลัพธ์ไปยังเทรดหลัก ซึ่งแอปจะแสดงผลลัพธ์ต่อผู้ใช้ได้
กล่าวคือ ViewModel
คอมโพเนนต์สถาปัตยกรรมจะเรียกเลเยอร์ที่เก็บในเทรดหลักเพื่อ
ทริกเกอร์คำขอเครือข่าย คู่มือนี้จะอธิบายโซลูชันต่างๆ
ที่ใช้โครูทีนเพื่อไม่ให้เทรดหลักถูกบล็อก
ViewModel มีชุดส่วนขยาย KTX ที่ทำงานร่วมกับ
โครูทีนโดยตรง ส่วนขยายเหล่านี้คือไลบรารี lifecycle-viewmodel-ktx และใช้ในคำแนะนำนี้
ข้อมูลการขึ้นต่อกัน
หากต้องการใช้โครูทีนในโปรเจ็กต์ Android ให้เพิ่มทรัพยากร Dependency ต่อไปนี้
ลงในไฟล์ build.gradle ของแอป
ดึงดูด
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
การดำเนินการในเธรดเบื้องหลัง
การส่งคำขอเครือข่ายในเทรดหลักจะทำให้เทรดหลักรอหรือบล็อก
จนกว่าจะได้รับการตอบกลับ เนื่องจากมีการบล็อกเธรด ระบบปฏิบัติการจึงเรียก onDraw() ไม่ได้
ซึ่งทำให้แอปค้างและอาจ
ทำให้เกิดกล่องโต้ตอบแอปพลิเคชันไม่ตอบสนอง (ANR) มาเรียกใช้การดำเนินการนี้ในเธรดเบื้องหลังเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีขึ้น
ก่อนอื่น มาดูคลาส Repository กันก่อนว่าคลาสนี้ส่งคำขอเครือข่ายอย่างไร
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // Function that makes the network request, blocking the current thread fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { val url = URL(loginUrl) (url.openConnection() as? HttpURLConnection)?.run { requestMethod = "POST" setRequestProperty("Content-Type", "application/json; utf-8") setRequestProperty("Accept", "application/json") doOutput = true outputStream.write(jsonBody.toByteArray()) return Result.Success(responseParser.parse(inputStream)) } return Result.Error(Exception("Cannot open HttpURLConnection")) } }
makeLoginRequest เป็นแบบซิงโครนัสและบล็อกเทรดที่เรียกใช้ เรามีคลาส Result ของเราเองเพื่อจำลอง
การตอบสนองของคำขอเครือข่าย
ViewModel จะทริกเกอร์คำขอเครือข่ายเมื่อผู้ใช้คลิก เช่น บนปุ่ม
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } }
โค้ดก่อนหน้า LoginViewModel จะบล็อกเทรด UI เมื่อ
ส่งคำขอเครือข่าย วิธีที่ง่ายที่สุดในการย้ายการดำเนินการ
ออกจากเทรดหลักคือการสร้างโครูทีนใหม่และดำเนินการคำขอเครือข่าย
ในเทรด I/O โดยทำดังนี้
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine to move the execution off the UI thread viewModelScope.launch(Dispatchers.IO) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } } }
มาดูโค้ดโครูทีนในฟังก์ชัน login กัน
viewModelScopeคือCoroutineScopeที่กำหนดไว้ล่วงหน้าซึ่งรวมอยู่ใน ส่วนขยายViewModelKTX โปรดทราบว่าโครูทีนทั้งหมดต้องทำงานใน ขอบเขตCoroutineScopeจัดการโครูทีนที่เกี่ยวข้องอย่างน้อย 1 รายการlaunchเป็นฟังก์ชันที่สร้างโครูทีนและส่งการ ดำเนินการของเนื้อหาฟังก์ชันไปยังตัวจัดสรรที่เกี่ยวข้องDispatchers.IOระบุว่าควรรันโครูทีนนี้ใน เทรดที่สงวนไว้สำหรับการดำเนินการ I/O
ฟังก์ชัน login จะดำเนินการดังนี้
- แอปเรียกใช้ฟังก์ชัน
loginจากเลเยอร์Viewในเทรดหลัก launchจะสร้างโครูทีนใหม่และส่งคำขอเครือข่าย โดยอิสระในเธรดที่สงวนไว้สำหรับการดำเนินการ I/O- ขณะที่โครูทีนทำงาน
loginฟังก์ชันจะดำเนินการต่อ และแสดงผล ซึ่งอาจเกิดขึ้นก่อนที่คำขอเครือข่ายจะเสร็จสิ้น โปรดทราบว่า เพื่อความง่าย เราจะละเว้นการตอบกลับของเครือข่ายไปก่อน
เนื่องจากโครูทีนนี้เริ่มต้นด้วย viewModelScope จึงมีการเรียกใช้ใน
ขอบเขตของ ViewModel หาก ViewModel ถูกทำลายเนื่องจากผู้ใช้เลื่อนออกจากหน้าจอ ระบบจะยกเลิก viewModelScope โดยอัตโนมัติ และยกเลิกโครูทีนที่กำลังทำงานทั้งหมดด้วย
ปัญหาอย่างหนึ่งของตัวอย่างก่อนหน้าคือทุกอย่างที่เรียกใช้
makeLoginRequest ต้องจำไว้ว่าต้องย้ายการดำเนินการออกจาก
เธรดหลักอย่างชัดเจน มาดูกันว่าเราจะแก้ไข Repository เพื่อแก้ปัญหานี้ให้เราได้อย่างไร
ใช้โครูทีนเพื่อความปลอดภัยของ Main
เราถือว่าฟังก์ชันเป็น main-safe เมื่อฟังก์ชันนั้นไม่บล็อกการอัปเดต UI ใน
เทรดหลัก ฟังก์ชัน makeLoginRequest ไม่ใช่ฟังก์ชันที่ปลอดภัยในเธรดหลัก เนื่องจากเมื่อเรียกใช้
makeLoginRequest จากเธรดหลัก จะบล็อก UI ใช้ฟังก์ชัน
withContext() จากไลบรารีโครูทีนเพื่อย้ายการดำเนินการ
ของโครูทีนไปยังเธรดอื่น
class LoginRepository( // ... ) { // ... suspend fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { // Move the execution of the coroutine to the I/O dispatcher return withContext(Dispatchers.IO) { // Blocking network request code } } }
withContext(Dispatchers.IO) จะย้ายการดำเนินการของโครูทีนไปยัง
เธรด I/O ซึ่งทำให้ฟังก์ชันการเรียกของเราปลอดภัยในเธรดหลักและช่วยให้ UI
อัปเดตได้ตามต้องการ
makeLoginRequest จะมีเครื่องหมายคีย์เวิร์ด suspend ด้วย คีย์เวิร์ดนี้
เป็นวิธีของ Kotlin ในการบังคับให้เรียกฟังก์ชันจากภายในโครูทีน
ในตัวอย่างต่อไปนี้ ระบบจะสร้างโครูทีนใน LoginViewModel
เนื่องจาก makeLoginRequest ย้ายการดำเนินการออกจากเทรดหลัก ตอนนี้จึงสามารถเรียกใช้โครูทีน
ในฟังก์ชัน login ในเทรดหลักได้
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine on the UI thread viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" // Make the network call and suspend execution until it finishes val result = loginRepository.makeLoginRequest(jsonBody) // Display result of the network request to the user when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
โปรดทราบว่าคุณยังคงต้องใช้โครูทีนที่นี่ เนื่องจาก makeLoginRequest เป็น
ฟังก์ชัน suspend และต้องเรียกใช้ฟังก์ชัน suspend ทั้งหมดใน
โครูทีน
โค้ดนี้แตกต่างจากตัวอย่าง login ก่อนหน้าใน 2 ประการ ดังนี้
launchไม่รับพารามิเตอร์Dispatchers.IOเมื่อคุณไม่ ส่งDispatcherไปยังlaunchโครูทีนใดก็ตามที่เปิดจากviewModelScopeจะทํางานในเทรดหลัก- ตอนนี้ระบบจะจัดการผลลัพธ์ของคำขอเครือข่ายเพื่อแสดง UI ที่ระบุว่าสำเร็จ หรือล้มเหลว
ตอนนี้ฟังก์ชันการเข้าสู่ระบบจะทำงานดังนี้
- แอปเรียกใช้ฟังก์ชัน
login()จากเลเยอร์Viewในเทรดหลัก launchจะสร้างโครูทีนใหม่ในเทรดหลัก และโครูทีนจะเริ่ม การดำเนินการ- ภายในโครูทีน การเรียกใช้
loginRepository.makeLoginRequest()now จะระงับการดำเนินการโครูทีนเพิ่มเติมจนกว่าบล็อกwithContextในmakeLoginRequest()จะทำงานเสร็จ - เมื่อบล็อก
withContextเสร็จสิ้น โครูทีนในlogin()จะกลับมาดำเนินการต่อในชุดข้อความหลักพร้อมผลลัพธ์ของคำขอเครือข่าย
การจัดการข้อยกเว้น
หากต้องการจัดการข้อยกเว้นที่เลเยอร์ Repository อาจส่งคืน ให้ใช้การรองรับข้อยกเว้นในตัวของ Kotlin
ในตัวอย่างต่อไปนี้ เราใช้บล็อก try-catch
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" val result = try { loginRepository.makeLoginRequest(jsonBody) } catch (e: Exception) { Result.Error(Exception("Network request failed")) } when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
ในตัวอย่างนี้ ระบบจะจัดการข้อยกเว้นที่ไม่คาดคิดที่เกิดจาก makeLoginRequest()
call เป็นข้อผิดพลาดใน UI
แหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีน
ดูรายละเอียดเพิ่มเติมเกี่ยวกับ Coroutines ใน Android ได้ที่ ปรับปรุงประสิทธิภาพของแอปด้วย Kotlin Coroutines
ดูแหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้ที่ลิงก์ต่อไปนี้
- ภาพรวมของ Coroutines (JetBrains)
- คู่มือ Coroutines (JetBrains)
- แหล่งข้อมูลเพิ่มเติมสำหรับโครูทีนและโฟลว์ของ Kotlin