โครูทีน Kotlin ใน Android

โครูทีนคือรูปแบบการออกแบบการทำงานพร้อมกันที่คุณใช้ใน 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 ที่กำหนดไว้ล่วงหน้าซึ่งรวมอยู่ใน ส่วนขยาย ViewModel KTX โปรดทราบว่าโครูทีนทั้งหมดต้องทำงานใน ขอบเขต 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

ดูแหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้ที่ลิงก์ต่อไปนี้