หน้านี้แสดงแนวทางปฏิบัติแนะนำหลายข้อที่จะส่งผลดีต่อแอปของคุณด้วยการทำให้แอปปรับขนาดได้ง่ายขึ้นและทดสอบได้ง่ายขึ้นเมื่อใช้ Coroutine
แทรก Dispatcher
อย่าฮาร์ดโค้ด Dispatchers เมื่อสร้าง Coroutine ใหม่หรือเรียกใช้ 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) { /* ... */ } }
รูปแบบการแทรกการพึ่งพานี้ช่วยให้การทดสอบง่ายขึ้น เนื่องจากคุณสามารถแทนที่ Dispatcher เหล่านั้นในการทดสอบหน่วยและการทดสอบการทำงานของเครื่องมือด้วย Dispatcher สำหรับการทดสอบ เพื่อให้การทดสอบมีความแน่นอนมากขึ้น
ฟังก์ชันระงับควรเรียกใช้จากเทรดหลักได้อย่างปลอดภัย
ฟังก์ชันระงับควรปลอดภัยต่อการเรียกใช้จากเทรดหลัก ซึ่งหมายความว่าเรียกใช้จากเทรดหลักได้อย่างปลอดภัย หากคลาสกำลังดำเนินการบล็อกที่ใช้เวลานานใน 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(): Result<List<ArticleWithAuthor>> { val news = newsRepository.fetchLatestNews() val response = mutableListOf<ArticleWithAuthor>() for (article in news) { val author = authorsRepository.getAuthor(article.author) response.add(ArticleWithAuthor(article, author)) } return Result.Success(response) } }
รูปแบบนี้ทำให้แอปปรับขนาดได้ง่ายขึ้น เนื่องจากคลาสที่เรียกใช้ฟังก์ชันระงับไม่ต้องกังวลว่าจะใช้ Dispatcher ใดกับงานประเภทใด ความรับผิดชอบนี้จะตกอยู่ที่คลาสที่ทำงาน
ViewModel ควรสร้าง Coroutine
ViewModel คลาสควรสร้าง Coroutine แทนที่จะเปิดเผยฟังก์ชันระงับเพื่อดำเนินการตรรกะทางธุรกิจ ฟังก์ชันระงับใน 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() }
มุมมองไม่ควรทริกเกอร์ Coroutine โดยตรงเพื่อดำเนินการตรรกะทางธุรกิจ
แต่ให้มอบความรับผิดชอบนั้นให้กับ ViewModel วิธีนี้จะช่วยให้ทดสอบตรรกะทางธุรกิจได้ง่ายขึ้น เนื่องจากออบเจ็กต์ ViewModel สามารถทดสอบหน่วยได้แทนที่จะใช้การทดสอบการทำงานของเครื่องมือที่จำเป็นสำหรับการทดสอบมุมมอง
นอกจากนี้ Coroutine จะยังคงอยู่ได้แม้จะมีการเปลี่ยนแปลงการกำหนดค่าโดยอัตโนมัติหากเริ่มงานใน viewModelScope หากสร้าง Coroutine โดยใช้ lifecycleScope แทน คุณจะต้องจัดการด้วยตนเอง
หาก Coroutine ต้องมีอายุการใช้งานนานกว่าขอบเขตของ ViewModel โปรดดูส่วน
การสร้าง Coroutine ในเลเยอร์ธุรกิจและเลเยอร์ข้อมูล
อย่าเปิดเผยประเภทที่เปลี่ยนแปลงได้
ควรเปิดเผยประเภทที่เปลี่ยนแปลงไม่ได้ให้กับคลาสอื่นๆ วิธีนี้จะรวมการเปลี่ยนแปลงทั้งหมดในประเภทที่เปลี่ยนแปลงได้ไว้ในคลาสเดียว ซึ่งจะช่วยให้แก้ไขข้อบกพร่องได้ง่ายขึ้นเมื่อเกิดข้อผิดพลาด
// 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) /* ... */ }
เลเยอร์ข้อมูลและเลเยอร์ธุรกิจควรเปิดเผยฟังก์ชันระงับและ Flow
โดยทั่วไปแล้ว คลาสในเลเยอร์ข้อมูลและเลเยอร์ธุรกิจจะเปิดเผยฟังก์ชันเพื่อทำการเรียกใช้แบบครั้งเดียวหรือเพื่อรับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงข้อมูลเมื่อเวลาผ่านไป คลาสในเลเยอร์เหล่านั้นควรเปิดเผยฟังก์ชันระงับสำหรับการเรียกใช้แบบครั้งเดียว และ Flow เพื่อแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงข้อมูล
// Classes in the data and business layer expose // either suspend functions or Flows class ExampleRepository { suspend fun makeNetworkRequest() { /* ... */ } fun getExamples(): Flow<Example> { /* ... */ } }
แนวทางปฏิบัติแนะนำนี้ช่วยให้ผู้เรียกใช้ ซึ่งโดยทั่วไปคือเลเยอร์การนำเสนอ สามารถควบคุมการดำเนินการและวงจรการทำงานของงานที่เกิดขึ้นในเลเยอร์เหล่านั้น รวมถึงยกเลิกได้เมื่อจำเป็น
การสร้าง Coroutine ในเลเยอร์ธุรกิจและเลเยอร์ข้อมูล
สำหรับคลาสในเลเยอร์ข้อมูลหรือเลเยอร์ธุรกิจที่ต้องสร้าง Coroutine ด้วยเหตุผลต่างๆ กัน คุณมีตัวเลือกต่างๆ ดังนี้
หากงานที่จะทำใน Coroutine เหล่านั้นเกี่ยวข้องเฉพาะเมื่อผู้ใช้อยู่ในหน้าจอปัจจุบัน งานควรเป็นไปตามวงจรการทำงานของผู้เรียกใช้ ในกรณีส่วนใหญ่ ผู้เรียกใช้จะเป็น 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 ภายนอกตามที่
อธิบายไว้ใน บล็อกโพสต์ Coroutine และรูปแบบสำหรับงานที่ไม่ควรยกเลิก
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 ที่กำหนดขอบเขตเป็นกราฟการนำทาง
แทรก TestDispatcher ในการทดสอบ
ควรแทรกอินสแตนซ์ของ
TestDispatcher
ลงในคลาสของคุณในการทดสอบ โดยมี 2 การติดตั้งใช้งานที่พร้อมใช้งานใน
ไลบรารี
kotlinx-coroutines-test ดังนี้
StandardTestDispatcher: จัดคิว Coroutine ที่เริ่มต้นใน Dispatcher ด้วยตัวจัดกำหนดการ และดำเนินการเมื่อเทรดการทดสอบไม่ว่าง คุณสามารถระงับเทรดการทดสอบเพื่อให้ Coroutine อื่นๆ ที่จัดคิวไว้ทำงานได้โดยใช้วิธีการต่างๆ เช่นadvanceUntilIdleUnconfinedTestDispatcher: เรียกใช้ Coroutine ใหม่ทันทีในลักษณะการบล็อก โดยทั่วไปวิธีนี้จะช่วยให้เขียนการทดสอบได้ง่ายขึ้น แต่คุณจะควบคุมวิธีที่ Coroutine ทำงานระหว่างการทดสอบได้น้อยลง
ดูรายละเอียดเพิ่มเติมได้ในเอกสารประกอบของการติดตั้งใช้งาน Dispatcher แต่ละรายการ
หากต้องการทดสอบ Coroutine ให้ใช้ตัวสร้าง Coroutine
runTest
runTest ใช้
TestCoroutineScheduler
เพื่อข้ามการหน่วงเวลาในการทดสอบและช่วยให้คุณควบคุมเวลาเสมือนได้ นอกจากนี้ คุณยังใช้ตัวจัดกำหนดการนี้เพื่อสร้าง Dispatcher สำหรับการทดสอบเพิ่มเติมได้ตามต้องการ
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, defaultDispatcher = testDispatcher ) val article = Article() repository.bookmarkArticle(article) assertThat(articlesDataSource.isBookmarked(article)).isTrue() } }
TestDispatchers ทั้งหมดควรใช้ตัวจัดกำหนดการเดียวกัน ซึ่งจะช่วยให้คุณเรียกใช้โค้ด Coroutine ทั้งหมดในเทรดการทดสอบเดียวเพื่อให้การทดสอบมีความแน่นอน runTest จะรอให้ Coroutine ทั้งหมดที่อยู่ในตัวจัดกำหนดการเดียวกันหรือเป็น Coroutine ย่อยของ Coroutine การทดสอบทำงานเสร็จก่อนที่จะแสดงผลลัพธ์
หลีกเลี่ยง GlobalScope
ซึ่งคล้ายกับแนวทางปฏิบัติแนะนำ แทรก Dispatcher การใช้
GlobalScope,
จะเป็นการฮาร์ดโค้ด CoroutineScope ที่คลาสใช้ ซึ่งจะทำให้เกิดข้อเสียบางประการ
ดังนี้
ส่งเสริมการฮาร์ดโค้ดค่า หากฮาร์ดโค้ด
GlobalScopeคุณอาจฮาร์ดโค้ดDispatchersด้วยทำให้การทดสอบทำได้ยากมาก เนื่องจากโค้ดของคุณจะทำงานในขอบเขตที่ไม่ได้ควบคุม คุณจึงไม่สามารถควบคุมการดำเนินการได้
คุณไม่สามารถใช้
CoroutineContextทั่วไปเพื่อดำเนินการกับ Coroutine ทั้งหมดที่สร้างขึ้นในขอบเขตได้
ให้พิจารณาแทรก CoroutineScope สำหรับงานที่ต้องมีอายุการใช้งานนานกว่าขอบเขตปัจจุบันแทน โปรดดูส่วน
การสร้าง Coroutine ในเลเยอร์ธุรกิจและเลเยอร์ข้อมูล
เพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อนี้
// 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 และตัวเลือกอื่นได้ใน
บล็อกโพสต์ Coroutine และรูปแบบสำหรับงานที่ไม่ควรยกเลิก
ทำให้ Coroutine ยกเลิกได้
การยกเลิกใน Coroutine เป็นแบบร่วมมือ ซึ่งหมายความว่าเมื่อ Job ของ Coroutine ถูกยกเลิก Coroutine จะไม่ถูกยกเลิกจนกว่าจะระงับหรือตรวจสอบการยกเลิก หากคุณดำเนินการบล็อกใน Coroutine โปรดตรวจสอบว่า Coroutine ยกเลิกได้
ตัวอย่างเช่น หากคุณกำลังอ่านไฟล์หลายไฟล์จากดิสก์ ให้ตรวจสอบว่า Coroutine ถูกยกเลิกหรือไม่ก่อนที่จะเริ่มอ่านไฟล์แต่ละไฟล์ วิธีหนึ่งในการ
ตรวจสอบการยกเลิกคือการเรียกใช้
ensureActive
ฟังก์ชัน
someScope.launch { for (file in files) { ensureActive() // Check for cancellation readFile(file) } }
ฟังก์ชันระงับทั้งหมดจาก kotlinx.coroutines เช่น withContext และ delay จะยกเลิกได้ หาก Coroutine เรียกใช้ฟังก์ชันเหล่านี้ คุณไม่จำเป็นต้องดำเนินการเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการยกเลิกใน Coroutine ได้ใน บล็อกโพสต์การยกเลิกใน Coroutine
ระวังข้อยกเว้น
ข้อยกเว้นที่ไม่ได้จัดการซึ่งเกิดขึ้นใน Coroutine อาจทำให้แอปขัดข้องได้ หากมีแนวโน้มที่จะเกิดข้อยกเว้น ให้ดักจับข้อยกเว้นเหล่านั้นในเนื้อหาของ Coroutine ใดก็ตามที่สร้างขึ้นด้วย viewModelScope หรือ lifecycleScope
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { try { loginRepository.login(username, token) // Update UI, user logged in successfully } catch (exception: IOException) { // Update UI, login attempt failed } } } }
ดูข้อมูลเพิ่มเติมได้ในบล็อกโพสต์ ข้อยกเว้นใน Coroutine หรือ การจัดการข้อยกเว้น Coroutine ในเอกสารประกอบของ Kotlin
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Coroutine
ดูแหล่งข้อมูล Coroutine เพิ่มเติมได้ที่ หน้าแหล่งข้อมูลเพิ่มเติมสำหรับ Kotlin Coroutine และ Flow