ใช้ Kotlin Coroutines กับคอมโพเนนต์ที่รับรู้ถึงวงจร (มุมมอง)

แนวคิดและการติดตั้งใช้งาน Jetpack Compose

โครูทีน Kotlin มี API ที่ช่วยให้คุณเขียนโค้ดแบบอะซิงโครนัสได้ เมื่อใช้โครูทีน Kotlin คุณจะกำหนด CoroutineScope ซึ่งช่วยให้คุณจัดการเวลาที่โครูทีนควรทำงานได้ การดำเนินการแบบไม่พร้อมกันแต่ละรายการ จะทำงานภายในขอบเขตที่เฉพาะเจาะจง

คอมโพเนนต์ที่รับรู้ถึงวงจรชีวิตให้การรองรับโครูทีนระดับเฟิร์สคลาสสำหรับขอบเขตเชิงตรรกะในแอปของคุณ รวมถึงเลเยอร์ความสามารถในการทำงานร่วมกันกับ LiveData หัวข้อนี้อธิบายวิธีใช้ Coroutine อย่างมีประสิทธิภาพกับคอมโพเนนต์ที่รับรู้ถึงวงจร

เพิ่มการขึ้นต่อกันของ KTX

Coroutine Scope ในตัวที่อธิบายไว้ในหัวข้อนี้จะอยู่ในส่วนขยาย KTX สำหรับคอมโพเนนต์ที่เกี่ยวข้องแต่ละรายการ อย่าลืมเพิ่มการอ้างอิงที่เหมาะสมเมื่อใช้ขอบเขตเหล่านี้

  • สำหรับ ViewModelScope ให้ใช้ androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 ขึ้นไป
  • สำหรับ LifecycleScope ให้ใช้ androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 ขึ้นไป
  • สำหรับ liveData ให้ใช้ androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 ขึ้นไป

ขอบเขตของโครูทีนที่รับรู้ถึงวงจร

คอมโพเนนต์ที่รับรู้ถึงวงจรจะกำหนดขอบเขตในตัวต่อไปนี้ที่คุณใช้ได้ ในแอป

ViewModelScope

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

คุณเข้าถึง CoroutineScope ของ ViewModel ได้ผ่านพร็อพเพอร์ตี้ viewModelScope ของ ViewModel ดังที่แสดงในตัวอย่างต่อไปนี้

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

LifecycleScope

มีการกำหนด LifecycleScope สำหรับออบเจ็กต์ Lifecycle แต่ละรายการ ระบบจะยกเลิกโครูทีนที่เปิดใช้ในขอบเขตนี้เมื่อทำลาย Lifecycle คุณเข้าถึง CoroutineScope ของ Lifecycle ได้ผ่านพร็อพเพอร์ตี้ lifecycle.coroutineScope หรือ lifecycleOwner.lifecycleScope

ตัวอย่างด้านล่างแสดงวิธีใช้ lifecycleOwner.lifecycleScope เพื่อสร้างข้อความที่คำนวณล่วงหน้าแบบไม่พร้อมกัน

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

โครูทีนที่รับรู้ถึงวงจรที่เริ่มใหม่ได้

แม้ว่า lifecycleScope จะมีวิธีที่เหมาะสมในการยกเลิกการดำเนินการที่ใช้เวลานานโดยอัตโนมัติเมื่อ Lifecycle เป็น DESTROYED แต่คุณอาจมีกรณีอื่นๆ ที่ต้องการเริ่มการดำเนินการของโค้ดบล็อกเมื่อ Lifecycle อยู่ในสถานะหนึ่ง และยกเลิกเมื่ออยู่ในอีกสถานะหนึ่ง ตัวอย่างเช่น คุณอาจต้องการรวบรวมโฟลว์เมื่อ Lifecycle เป็น STARTED และยกเลิกการรวบรวมเมื่อเป็น STOPPED แนวทางนี้จะประมวลผลการไหลของ การปล่อยข้อมูลเมื่อ UI ปรากฏบนหน้าจอเท่านั้น ซึ่งจะช่วยประหยัดทรัพยากรและ อาจหลีกเลี่ยงการขัดข้องของแอปได้

สำหรับกรณีเหล่านี้ Lifecycle และ LifecycleOwner มี API suspend repeatOnLifecycle ที่ทำหน้าที่ดังกล่าว ตัวอย่างต่อไปนี้มีโค้ดบล็อกที่ทำงานทุกครั้งที่ Lifecycle ที่เชื่อมโยงมีสถานะเป็นSTARTEDอย่างน้อย และยกเลิกเมื่อ Lifecycle มีสถานะเป็นSTOPPED

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

การรวบรวมโฟลว์ที่รับรู้ถึงวงจร

หากต้องการดำเนินการคอลเล็กชันที่รับรู้ถึงวงจรขององค์ประกอบในโฟลว์เดียว คุณสามารถ ใช้วิธี Flow.flowWithLifecycle() เพื่อลดความซับซ้อนของโค้ดได้

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

อย่างไรก็ตาม หากต้องการดำเนินการรวบรวมที่รับรู้ถึงวงจรขององค์ประกอบในหลายโฟลว์แบบขนานกัน คุณต้องรวบรวมแต่ละโฟลว์ในโครูทีนที่แตกต่างกัน ในกรณีนี้ การใช้ repeatOnLifecycle() โดยตรงจะมีประสิทธิภาพมากกว่า

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

ระงับโครูทีนที่รับรู้ถึงวงจร

แม้ว่า CoroutineScope จะเป็นวิธีที่เหมาะสมในการยกเลิกการดำเนินการที่ใช้เวลานานโดยอัตโนมัติ แต่คุณอาจมีกรณีอื่นๆ ที่ต้องการระงับการดำเนินการของโค้ดบล็อก เว้นแต่ว่า Lifecycle จะอยู่ในสถานะหนึ่งๆ ตัวอย่างเช่น หากต้องการเรียกใช้ FragmentTransaction คุณต้องรอจนกว่า Lifecycle จะมีอายุอย่างน้อย STARTED สำหรับกรณีเหล่านี้ Lifecycle มีวิธีการเพิ่มเติมให้ ดังนี้ lifecycle.whenCreated, lifecycle.whenStarted และ lifecycle.whenResumed โครูทีนที่ทำงานภายในบล็อกเหล่านี้จะถูกระงับหาก Lifecycle ไม่ได้อยู่ในสถานะที่ต้องการขั้นต่ำ

ตัวอย่างด้านล่างมีโค้ดบล็อกที่จะทำงานเฉพาะเมื่อLifecycleที่เชื่อมโยงกันอยู่ในสถานะSTARTEDเป็นอย่างน้อย

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

หาก Lifecycle ถูกทำลายในขณะที่โครูทีนทำงานผ่านเมธอดใดเมธอดหนึ่งของ when โครูทีนจะถูกยกเลิกโดยอัตโนมัติ ในตัวอย่างด้านล่าง บล็อก finally จะทํางานเมื่อสถานะ Lifecycle เป็น DESTROYED

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

ใช้โครูทีนกับ LiveData

เมื่อใช้ LiveData คุณอาจต้องคำนวณค่าแบบไม่พร้อมกัน เช่น คุณอาจต้องการดึงค่ากําหนดของผู้ใช้และแสดงค่ากําหนดเหล่านั้นใน UI ของคุณ ในกรณีเช่นนี้ คุณสามารถใช้ฟังก์ชันตัวสร้าง liveData เพื่อเรียกฟังก์ชัน suspend โดยแสดงผลลัพธ์เป็นออบเจ็กต์ LiveData

ในตัวอย่างด้านล่าง loadUser() คือฟังก์ชันระงับที่ประกาศไว้ที่อื่น ใช้ฟังก์ชันตัวสร้าง liveData เพื่อเรียก loadUser() แบบอะซิงโครนัส แล้วใช้ emit() เพื่อส่งผลลัพธ์

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

liveData องค์ประกอบที่ใช้สร้างสรรค์ทำหน้าที่เป็นการทำงานพร้อมกันที่มีโครงสร้างดั้งเดิมระหว่างโครูทีนกับ LiveData โค้ดบล็อกจะเริ่มทำงานเมื่อ LiveData ใช้งานได้ และจะยกเลิกโดยอัตโนมัติหลังจากหมดเวลาที่กำหนดค่าได้เมื่อ LiveData ใช้งานไม่ได้ หากมีการยกเลิกก่อนเสร็จสมบูรณ์ ระบบจะรีสตาร์ทหากLiveDataกลับมาใช้งานได้ อีกครั้ง หากการเรียกใช้ก่อนหน้านี้เสร็จสมบูรณ์แล้ว ระบบจะไม่เริ่มการเรียกใช้ใหม่ โปรดทราบ ว่าระบบจะรีสตาร์ทก็ต่อเมื่อมีการยกเลิกโดยอัตโนมัติเท่านั้น หากการบล็อกถูกยกเลิก ด้วยเหตุผลอื่น (เช่น การCancellationException) ระบบจะไม่ เริ่มการบล็อกใหม่

นอกจากนี้ คุณยังส่งค่าหลายค่าจากบล็อกได้ด้วย emit() การเรียกแต่ละครั้งจะระงับ การดำเนินการของบล็อกจนกว่าจะมีการตั้งค่า LiveData ในเทรดหลัก

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

นอกจากนี้ คุณยังใช้ liveData ร่วมกับ Transformations ได้ด้วย ดังตัวอย่างต่อไปนี้

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

คุณสามารถส่งค่าหลายค่าจาก LiveData ได้โดยการเรียกใช้ฟังก์ชัน emitSource() เมื่อใดก็ตามที่ต้องการส่งค่าใหม่ โปรดทราบว่าการเรียกใช้ emit() หรือ emitSource() แต่ละครั้งจะนำแหล่งที่มาที่เพิ่มไว้ก่อนหน้านี้ออก

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

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

แหล่งข้อมูลเพิ่มเติม

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

ตัวอย่าง

บล็อก