โครูทีน Kotlin ใน Android

coroutine คือรูปแบบการออกแบบการเกิดขึ้นพร้อมกันที่คุณสามารถใช้ได้ Android เพื่อลดความซับซ้อนของโค้ดที่ทำงานแบบไม่พร้อมกัน Coroutines ได้รับการเพิ่มลงใน Kotlin เวอร์ชัน 1.3 และอิงตาม มาจากภาษาอื่นๆ

สำหรับ Android โครูทีนจะช่วยจัดการงานที่ต้องทำเป็นเวลานาน ซึ่งอาจ ไม่เช่นนั้นให้บล็อกเทรดหลัก และทำให้แอปไม่ตอบสนอง นักพัฒนาซอฟต์แวร์มืออาชีพกว่า 50% ที่ใช้โครูทีนได้รายงานว่าพบว่า ประสิทธิภาพการทำงานที่เพิ่มขึ้น หัวข้อนี้จะอธิบายวิธีใช้โครูทีน Kotlin เพื่อจัดการกับสิ่งเหล่านี้ โดยช่วยให้คุณเขียนโค้ดของแอปที่ดูสะอาดตาและกระชับยิ่งขึ้น

ฟีเจอร์

Coroutines เป็นโซลูชันที่เราแนะนำให้ใช้สำหรับการเขียนโปรแกรมแบบไม่พร้อมกันบน Android ฟีเจอร์ที่น่าสนใจมีดังต่อไปนี้

  • มีขนาดเล็ก: คุณสามารถเรียกใช้โครูทีนหลายรายการในชุดข้อความเดียวได้เนื่องจากเหตุผลต่อไปนี้ การสนับสนุนสำหรับ การระงับ ซึ่งไม่บล็อกเทรดที่โครูทีนทำงานอยู่ กำลังระงับ ประหยัดหน่วยความจำหลังการบล็อก ในขณะเดียวกันก็รองรับการทำงานพร้อมกันหลายรายการ
  • หน่วยความจำรั่วไหลน้อยลง: ใช้ การเกิดขึ้นพร้อมกันที่มีโครงสร้าง ในการดำเนินการภายในขอบเขต
  • การสนับสนุนการยกเลิกในตัว: การยกเลิก เผยแพร่โดยอัตโนมัติผ่านลำดับชั้น Coroutine ที่กำลังทำงานอยู่
  • การผสานรวม Jetpack: ไลบรารี Jetpack จำนวนมากประกอบด้วย ส่วนขยายที่ช่วยรองรับโครูทีนเต็มรูปแบบ ใช้บ้าง ห้องสมุดยังมีหมวดหมู่ของตนเอง ขอบเขต Coroutine ที่คุณสามารถ เพื่อใช้การเกิดขึ้นพร้อมกันแบบมีโครงสร้าง

ภาพรวมตัวอย่าง

ตัวอย่างตามคู่มือสถาปัตยกรรมแอป ในหัวข้อนี้ ให้ส่งคำขอเครือข่ายและส่งคืนผลลัพธ์ไปยัง เทรด ซึ่งแอปสามารถแสดงผลลัพธ์ให้กับผู้ใช้

กล่าวอย่างเจาะจงคือ ViewModel คอมโพเนนต์สถาปัตยกรรมจะเรียกใช้เลเยอร์ที่เก็บบนเทรดหลักเพื่อ เรียกใช้คำขอเครือข่าย คู่มือนี้จะทบทวนวิธีแก้ปัญหาต่างๆ ที่ใช้โครูทีนเพื่อเลิกบล็อกเทรดหลัก

ViewModel มีชุดส่วนขยาย KTX ที่ทํางานร่วมกับ โครูทีน ส่วนขยายเหล่านี้คือ คลัง lifecycle-viewmodel-ktx และมีการนำไปใช้แล้ว ในคู่มือนี้

ข้อมูลการขึ้นต่อกัน

หากต้องการใช้โครูทีนในโปรเจ็กต์ Android ให้เพิ่มทรัพยากร Dependency ต่อไปนี้ ลงในไฟล์ build.gradle ของแอป

Groovy

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>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // 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ที่กำหนดไว้ล่วงหน้าซึ่งรวมอยู่กับ ส่วนขยาย KTX ViewModel โปรดทราบว่าโครูทีนทั้งหมดต้องทํางานใน CoroutineScope จะจัดการโครูทีนที่เกี่ยวข้องอย่างน้อย 1 รายการ
  • launch คือฟังก์ชันที่สร้างโคโรทีนและส่งฟังก์ชัน ของส่วนการทำงานของฟังก์ชันไปยังผู้มอบหมายงานที่เกี่ยวข้อง
  • Dispatchers.IO บ่งชี้ว่าโครูทีนนี้ควรเรียกใช้ใน เทรดที่สงวนไว้สำหรับการดำเนินการ I/O

ฟังก์ชัน login จะดำเนินการดังต่อไปนี้

  • แอปเรียกฟังก์ชัน login จากเลเยอร์ View ในเทรดหลัก
  • launch สร้างโครูทีนใหม่และสร้างคำขอเครือข่าย แยกต่างหากในเทรดที่สงวนไว้สำหรับการดำเนินการ I/O
  • ขณะที่โครูทีนทำงานอยู่ ฟังก์ชัน login จะดำเนินการต่อไป และส่งกลับ ซึ่งเป็นไปได้ว่าก่อนที่คำขอเครือข่ายจะเสร็จสิ้น โปรดทราบว่า เพื่อความง่าย ตอนนี้ระบบจะไม่สนใจการตอบสนองของเครือข่าย

เนื่องจาก coroutine นี้เริ่มต้นด้วย viewModelScope จึงมีการดำเนินการใน ขอบเขตของ ViewModel หาก ViewModel ถูกทำลายเนื่องจาก ผู้ใช้ออกจากหน้าจอไป viewModelScope ทำงานโดยอัตโนมัติ ยกเลิกแล้ว และระบบก็จะยกเลิกโครูทีนทั้งหมดที่ทำงานอยู่ด้วย

ปัญหาหนึ่งในตัวอย่างก่อนหน้านี้คือ makeLoginRequest ต้องจดจำว่าต้องปิดการดำเนินการอย่างชัดเจน เทรดหลัก มาดูกันว่าเราจะแก้ไข Repository เพื่อแก้โจทย์ ปัญหานี้ให้กับเรา

ใช้โครูทีนเพื่อความปลอดภัยหลัก

เราจะถือว่าฟังก์ชันปลอดภัยหลักเมื่อไม่บล็อกการอัปเดต UI ใน เทรดหลัก ฟังก์ชัน makeLoginRequest ไม่ปลอดภัยหลักเนื่องจากการโทร makeLoginRequest จากเทรดหลักจะบล็อก UI ใช้เมนู ฟังก์ชัน withContext() จากไลบรารี Coroutines สำหรับย้ายการดำเนินการ โครูทีนไปยังชุดข้อความอื่น

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() ตอนนี้ระงับการดำเนินการโคโรทีนเพิ่มเติมจนกว่าจะถึง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() เป็นข้อผิดพลาดใน UI

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

ดูรายละเอียดเพิ่มเติมของโครูทีนใน Android ได้ที่ ปรับปรุงประสิทธิภาพของแอปด้วยโครูทีน Kotlin

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