แนวทางปฏิบัติแนะนำสำหรับโครูทีนใน Android

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

จ่ายงานฉีด

อย่าฮาร์ดโค้ด Dispatchers เมื่อสร้างโครูทีนใหม่หรือการเรียกใช้ withContext

// DO inject Dispatchers
class NewsRepository(
   
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
   
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

// DO NOT hardcode Dispatchers
class NewsRepository {
   
// DO NOT use Dispatchers.Default directly, inject it instead
   
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

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

ฟังก์ชันการระงับควรปลอดภัยในการเรียกจากเทรดหลัก

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

class NewsRepository(private val ioDispatcher: CoroutineDispatcher) {

   
// As this operation is manually retrieving the news from the server
   
// using a blocking HttpURLConnection, it needs to move the execution
   
// to an IO dispatcher to make it main-safe
   
suspend fun fetchLatestNews(): List<Article> {
        withContext
(ioDispatcher) { /* ... implementation ... */ }
   
}
}

// This use case fetches the latest news and the associated author.
class GetLatestNewsWithAuthorsUseCase(
   
private val newsRepository: NewsRepository,
   
private val authorsRepository: AuthorsRepository
) {
   
// This method doesn't need to worry about moving the execution of the
   
// coroutine to a different thread as newsRepository is main-safe.
   
// The work done in the coroutine is lightweight as it only creates
   
// a list and add elements to it
   
suspend operator fun invoke(): List<ArticleWithAuthor> {
       
val news = newsRepository.fetchLatestNews()

       
val response: List<ArticleWithAuthor> = mutableEmptyList()
       
for (article in news) {
           
val author = authorsRepository.getAuthor(article.author)
            response
.add(ArticleWithAuthor(article, author))
       
}
       
return Result.Success(response)
   
}
}

รูปแบบนี้จะทำให้แอปของคุณรองรับการปรับขนาดมากขึ้น เนื่องจากการเรียกใช้ฟังก์ชันจะมีการระงับคลาส ไม่ต้องกังวลว่า Dispatcher จะใช้อะไรสำหรับงานประเภทไหน ช่วงเวลานี้ ความรับผิดชอบนั้นแฝงอยู่ในชั้นเรียนที่ทำงาน

ViewModel ควรสร้างโครูทีน

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

// DO create coroutines in the ViewModel
class LatestNewsViewModel(
   
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {

   
private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
   
val uiState: StateFlow<LatestNewsUiState> = _uiState

   
fun loadNews() {
        viewModelScope
.launch {
           
val latestNewsWithAuthors = getLatestNewsWithAuthors()
           
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
       
}
   
}
}

// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
   
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
   
// DO NOT do this. News would probably need to be refreshed as well.
   
// Instead of exposing a single value with a suspend function, news should
   
// be exposed using a stream of data as in the code snippet above.
   
suspend fun loadNews() = getLatestNewsWithAuthors()
}

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

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

อย่าแสดงประเภทที่เปลี่ยนแปลงได้

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

// DO expose immutable types
class LatestNewsViewModel : ViewModel() {

   
private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
   
val uiState: StateFlow<LatestNewsUiState> = _uiState

   
/* ... */
}

class LatestNewsViewModel : ViewModel() {

   
// DO NOT expose mutable types
   
val uiState = MutableStateFlow(LatestNewsUiState.Loading)

   
/* ... */
}

ชั้นข้อมูลและชั้นธุรกิจควรแสดงฟังก์ชันและโฟลว์การระงับ

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

// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
   
suspend fun makeNetworkRequest() { /* ... */ }

   
fun getExamples(): Flow<Example> { /* ... */ }
}

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

การสร้างโครูทีนในธุรกิจและชั้นข้อมูล

สำหรับคลาสในเลเยอร์ข้อมูลหรือชั้นธุรกิจที่ต้องการสร้างโครูทีนสำหรับ เหตุผลต่างๆ ก็มีตัวเลือกที่ต่างกันไป

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

class GetAllBooksAndAuthorsUseCase(
   
private val booksRepository: BooksRepository,
   
private val authorsRepository: AuthorsRepository,
) {
   
suspend fun getBookAndAuthors(): BookAndAuthors {
       
// In parallel, fetch books and authors and return when both requests
       
// complete and the data is ready
       
return coroutineScope {
           
val books = async { booksRepository.getAllBooks() }
           
val authors = async { authorsRepository.getAllAuthors() }
           
BookAndAuthors(books.await(), authors.await())
       
}
   
}
}

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

class ArticlesRepository(
   
private val articlesDataSource: ArticlesDataSource,
   
private val externalScope: CoroutineScope,
) {
   
// As we want to complete bookmarking the article even if the user moves
   
// away from the screen, the work is done creating a new coroutine
   
// from an external scope
   
suspend fun bookmarkArticle(article: Article) {
        externalScope
.launch { articlesDataSource.bookmarkArticle(article) }
           
.join() // Wait for the coroutine to complete
   
}
}

externalScope ควรได้รับการสร้างและจัดการโดยชั้นเรียนที่มีอายุนานกว่า หน้าจอปัจจุบัน ซึ่งอาจจัดการโดยชั้นเรียน Application หรือ ViewModel กำหนดขอบเขตเป็นกราฟการนำทาง

แทรก TestDispatchers ในการทดสอบ

อินสแตนซ์ของ TestDispatcher ในชั้นเรียนของคุณ ในการทดสอบ มี 2 แบบ ใน คลัง kotlinx-coroutines-test:

  • StandardTestDispatcher: จัดคิวโครูทีนซึ่งเริ่มจากเครื่องจัดตารางเวลาและดำเนินการ เมื่อชุดข้อความทดสอบไม่ว่าง คุณสามารถระงับชุดข้อความทดสอบเพื่อ โครูทีนอื่นๆ ที่อยู่ในคิวทำงานโดยใช้วิธีต่างๆ เช่น advanceUntilIdle

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

โปรดดูรายละเอียดเพิ่มเติมในเอกสารประกอบการใช้งานของผู้มอบหมายงานแต่ละราย

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

class ArticlesRepositoryTest {

   
@Test
   
fun testBookmarkArticle() = runTest {
       
// Pass the testScheduler provided by runTest's coroutine scope to
       
// the test dispatcher
       
val testDispatcher = UnconfinedTestDispatcher(testScheduler)

       
val articlesDataSource = FakeArticlesDataSource()
       
val repository = ArticlesRepository(
            articlesDataSource
,
            testDispatcher
       
)
       
val article = Article()
        repository
.bookmarkArticle(article)
        assertThat
(articlesDataSource.isBookmarked(article)).isTrue()
   
}
}

TestDispatchers ทั้งหมดควรใช้เครื่องจัดตารางเวลาเดียวกัน วิธีนี้ช่วยให้คุณทำสิ่งต่อไปนี้ได้ รันโค้ด coroutine ทั้งหมดบนเธรดการทดสอบเดี่ยวเพื่อทดสอบของคุณ เชิงกำหนด runTest จะรอโครูทีนทั้งหมดที่อยู่เดียวกัน หรือเป็นลูกของโครูทีนทดสอบที่ต้องทำก่อนกลับมา

หลีกเลี่ยง GlobalScope

ซึ่งคล้ายกับแนวทางปฏิบัติแนะนำของ Inject Dispatchers โดยการใช้ GlobalScope คุณกำลังฮาร์ดโค้ด CoroutineScope ที่ชั้นเรียนใช้นำข้อเสียบางอย่าง ด้วย:

  • ส่งเสริมค่าฮาร์ดโค้ด หากคุณฮาร์ดโค้ด GlobalScope คุณอาจ ฮาร์ดโค้ด Dispatchers ด้วย

  • ทำให้การทดสอบยากมากเพราะโค้ดของคุณทำงานในขอบเขตที่ควบคุมไม่ได้ คุณจะไม่สามารถควบคุมการทำงานของระบบได้

  • คุณไม่สามารถใช้ CoroutineContext ทั่วไปกับโครูทีนทั้งหมด ไว้ในขอบเขตนั้น

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

// DO inject an external scope instead of using GlobalScope.
// GlobalScope can be used indirectly. Here as a default parameter makes sense.
class ArticlesRepository(
   
private val articlesDataSource: ArticlesDataSource,
   
private val externalScope: CoroutineScope = GlobalScope,
   
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
   
// As we want to complete bookmarking the article even if the user moves
   
// away from the screen, the work is done creating a new coroutine
   
// from an external scope
   
suspend fun bookmarkArticle(article: Article) {
        externalScope
.launch(defaultDispatcher) {
            articlesDataSource
.bookmarkArticle(article)
       
}
           
.join() // Wait for the coroutine to complete
   
}
}

// DO NOT use GlobalScope directly
class ArticlesRepository(
   
private val articlesDataSource: ArticlesDataSource,
) {
   
// As we want to complete bookmarking the article even if the user moves away
   
// from the screen, the work is done creating a new coroutine with GlobalScope
   
suspend fun bookmarkArticle(article: Article) {
       
GlobalScope.launch {
            articlesDataSource
.bookmarkArticle(article)
       
}
           
.join() // Wait for the coroutine to complete
   
}
}

ดูข้อมูลเพิ่มเติมเกี่ยวกับ GlobalScope และตัวเลือกอื่นๆ ใน Coroutines และ รูปแบบของงานที่ไม่ควรยกเลิกในบล็อกโพสต์

ทำให้โครูทีนไม่สามารถเก็บได้

การยกเลิกโครูทีนจะเป็นแบบสหกรณ์ ซึ่งหมายความว่าเมื่อโครูทีน ยกเลิกJobแล้ว โครูทีนจะไม่ยกเลิกจนกว่าจะระงับหรือตรวจสอบ เพื่อยกเลิก หากคุณบล็อกการดำเนินการในโครูทีน โครูทีนนั้น Cancellable ได้

เช่น หากกำลังอ่านไฟล์หลายไฟล์จากดิสก์ก่อนเริ่ม กำลังอ่านแต่ละไฟล์ แล้วดูว่าโครูทีนถูกยกเลิก (Coroutine) หรือไม่ วิธีเดียวในการ ตรวจสอบการยกเลิกคือโดยโทรไปที่ ensureActive

someScope.launch {
   
for(file in files) {
        ensureActive
() // Check for cancellation
        readFile
(file)
   
}
}

ฟังก์ชันระงับทั้งหมดจาก kotlinx.coroutines เช่น withContext และ delay ยกเลิกได้ คุณไม่ควรดำเนินการหากโครูทีนโทรหาพวกเขา งานอื่นๆ อีก

ดูข้อมูลเพิ่มเติมเกี่ยวกับการยกเลิกโครูทีนได้ที่ การยกเลิกในบล็อกโพสต์ Coroutine

ระวังข้อยกเว้น

ข้อยกเว้นที่ไม่มีการจัดการในโครูทีนอาจทำให้แอปขัดข้องได้ หากมีข้อยกเว้น ให้จับสลีปไว้ในร่างกายของโครูทีนที่สร้างขึ้นด้วย viewModelScope หรือ lifecycleScope

class LoginViewModel(
   
private val loginRepository: LoginRepository
) : ViewModel() {

   
fun login(username: String, token: String) {
        viewModelScope
.launch {
           
try {
                loginRepository
.login(username, token)
               
// Notify view user logged in successfully
           
} catch (exception: IOException) {
               
// Notify view login attempt failed
           
}
       
}
   
}
}

ดูข้อมูลเพิ่มเติมได้จากบล็อกโพสต์ ข้อยกเว้นในโครูทีน หรือการจัดการข้อยกเว้น Coroutine ในเอกสารประกอบของ Kotlin

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

สำหรับแหล่งข้อมูลเกี่ยวกับโครูทีนเพิ่มเติม ให้ดูที่ แหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนและโฟลว์ของ Kotlin