โครูทีนคือรูปแบบการออกแบบการทำงานพร้อมกันที่คุณใช้ใน Android เพื่อลดความซับซ้อนของโค้ดที่ทำงานแบบไม่พร้อมกันได้ โครูทีน ได้รับการเพิ่มลงใน Kotlin ในเวอร์ชัน 1.3 และอิงตามแนวคิดที่ได้รับการยอมรับ จากภาษาอื่นๆ
ใน Android คอร์รูทีนช่วยจัดการงานที่ใช้เวลานานซึ่งอาจบล็อกเทรดหลักและทำให้แอปไม่ตอบสนอง นักพัฒนาซอฟต์แวร์มืออาชีพกว่า 50% ที่ใช้ Coroutine รายงานว่า ประสิทธิภาพการทำงานเพิ่มขึ้น หัวข้อนี้อธิบายวิธีใช้โคโรทีน Kotlin เพื่อแก้ปัญหาเหล่านี้ ซึ่งจะช่วยให้คุณเขียนโค้ดแอปได้สะอาดและกระชับยิ่งขึ้น
ฟีเจอร์
โครูทีนเป็นโซลูชันที่แนะนำสำหรับการเขียนโปรแกรมแบบอะซิงโครนัสใน Android ฟีเจอร์ที่น่าสนใจมีดังนี้
- น้ำหนักเบา: คุณเรียกใช้โครูทีนจำนวนมากในเธรดเดียวได้เนื่องจาก รองรับ การระงับ ซึ่งจะไม่บล็อกเธรดที่โครูทีนกำลังทำงานอยู่ การระงับ ช่วยประหยัดหน่วยความจำมากกว่าการบล็อกในขณะที่รองรับการดำเนินการพร้อมกันจำนวนมาก
- หน่วยความจำรั่วไหลน้อยลง: ใช้การทำงานพร้อมกันที่มีโครงสร้าง เพื่อเรียกใช้การดำเนินการภายในขอบเขต
- การรองรับการยกเลิกในตัว: การยกเลิก จะเผยแพร่โดยอัตโนมัติผ่านลำดับชั้นของโครูทีนที่ทำงานอยู่
- การผสานรวม Jetpack: ไลบรารี Jetpack หลายรายการมีส่วนขยายที่รองรับโครูทีนอย่างเต็มรูปแบบ ไลบรารีบางรายการยังมีขอบเขตของโครูทีนของตัวเองที่คุณใช้เพื่อการทำงานพร้อมกันที่มีโครงสร้างได้ด้วย
ภาพรวมตัวอย่าง
ตามคำแนะนำเกี่ยวกับสถาปัตยกรรมของแอป ตัวอย่างในหัวข้อนี้จะส่งคำขอเครือข่ายและแสดงผลลัพธ์ไปยังเทรดหลัก ซึ่งแอปจะแสดงผลลัพธ์ต่อผู้ใช้ได้
กล่าวคือ ViewModel
คอมโพเนนต์สถาปัตยกรรมจะเรียกเลเยอร์ที่เก็บในเทรดหลักเพื่อ
ทริกเกอร์คำขอเครือข่าย คู่มือนี้จะอธิบายโซลูชันต่างๆ
ที่ใช้โครูทีนเพื่อไม่ให้เทรดหลักถูกบล็อก
ViewModel มีชุดส่วนขยาย KTX ที่ทำงานร่วมกับ
โครูทีนโดยตรง ส่วนขยายเหล่านี้คือ
ไลบรารี lifecycle-viewmodel-ktx และใช้
ในคู่มือนี้
ข้อมูลทรัพยากร Dependency
หากต้องการใช้โครูทีนในโปรเจ็กต์ 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 suspends การดำเนินการโครูทีนเพิ่มเติมจนกว่า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
ดูแหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้ที่ลิงก์ต่อไปนี้