หน้านี้นำเสนอแนวทางปฏิบัติที่ดีที่สุดหลายข้อ ซึ่งมีผลกระทบในเชิงบวกโดย แอปรองรับการปรับขนาดและทดสอบได้มากขึ้นเมื่อใช้โครูทีน
จ่ายงานฉีด
อย่าฮาร์ดโค้ด 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