Halaman ini menyajikan beberapa praktik terbaik yang memiliki dampak positif dengan membuat aplikasi Anda lebih skalabel dan dapat diuji saat menggunakan coroutine.
Memasukkan Dispatcher
Jangan meng-hardcode Dispatchers
saat membuat coroutine baru atau memanggil
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) { /* ... */ }
}
Pola injeksi dependensi ini mempermudah pengujian karena Anda dapat mengganti dispatcher tersebut dalam uji unit dan instrumentasi dengan dispatcher pengujian untuk membuat pengujian Anda menjadi lebih deterministik.
Fungsi penangguhan harus aman untuk dipanggil dari thread utama
Fungsi penangguhan harus main-safe, artinya aman untuk dipanggil dari
thread utama. Jika suatu class melakukan operasi pemblokiran yang berjalan lama
di coroutine, fungsi penangguhan bertugas memindahkan eksekusi dari thread utama menggunakan
withContext
. Ini berlaku untuk semua class di aplikasi Anda, terlepas dari bagian
arsitektur tempat class tersebut berada.
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)
}
}
Pola ini membuat aplikasi Anda lebih skalabel, karena class yang memanggil fungsi penangguhan
tidak perlu mengkhawatirkan Dispatcher
yang akan digunakan untuk suatu jenis pekerjaan. Tanggung jawab
ini berada di class yang mengerjakan pekerjaan.
ViewModel harus membuat coroutine
Class ViewModel
sebaiknya memilih
membuat coroutine daripada mengekspos fungsi penangguhan untuk menjalankan logika
bisnis. Fungsi penangguhan di ViewModel
dapat berguna jika bukan
mengekspos status menggunakan aliran data, hanya satu nilai yang perlu dimunculkan.
// 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()
}
Tampilan tidak boleh langsung memicu coroutine untuk menjalankan logika bisnis.
Sebagai gantinya, tangguhkan tanggung jawab tersebut ke ViewModel
. Hal ini membuat logika
bisnis lebih mudah diuji karena objek ViewModel
dapat diuji unitnya, bukan menggunakan
uji instrumentasi yang diperlukan untuk menguji tampilan.
Selain itu, coroutine Anda akan tetap berfungsi meski konfigurasi otomatis berubah
jika pekerjaan dimulai di viewModelScope
. Jika Anda membuat
coroutine menggunakan lifecycleScope
, Anda harus menanganinya secara manual.
Jika coroutine perlu aktif lebih lama dari cakupan ViewModel
, lihat
Bagian membuat coroutine di lapisan bisnis dan data.
Jangan mengekspos jenis yang dapat diubah
Pilih mengekspos jenis yang tidak dapat diubah ke class lain. Dengan demikian, semua perubahan pada jenis yang dapat diubah terpusat di satu class akan memudahkan proses debug saat terjadi kesalahan.
// 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)
/* ... */
}
Lapisan data dan bisnis harus mengekspos fungsi penangguhan dan Alur
Class di lapisan data dan bisnis umumnya mengekspos fungsi untuk melakukan panggilan satu kali atau agar diberi tahu tentang perubahan data dari waktu ke waktu. Class di lapisan tersebut harus mengekspos fungsi penangguhan untuk panggilan satu kali dan Alur untuk memberi tahu tentang perubahan data.
// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
suspend fun makeNetworkRequest() { /* ... */ }
fun getExamples(): Flow<Example> { /* ... */ }
}
Praktik terbaik ini membuat pemanggil, biasanya lapisan presentasi, dapat mengontrol eksekusi dan siklus proses pekerjaan yang terjadi dalam lapisan tersebut, dan membatalkannya jika diperlukan.
Membuat coroutine di lapisan bisnis dan data
Untuk class di lapisan data atau bisnis yang perlu membuat coroutine karena alasan yang berbeda, terdapat opsi yang berbeda pula.
Jika pekerjaan yang akan dilakukan di coroutine tersebut hanya relevan saat pengguna
ada di layar saat ini, pekerjaan tersebut harus mengikuti siklus proses pemanggil. Dalam
sebagian besar kasus, pemanggil akan menjadi ViewModel, dan panggilan akan dibatalkan saat
pengguna keluar dari layar dan ViewModel dihapus. Dalam hal ini,
coroutineScope
atau supervisorScope
harus digunakan.
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())
}
}
}
Jika pekerjaan yang akan dilakukan relevan selama aplikasi dibuka, dan pekerjaan
tidak terikat ke layar tertentu, pekerjaan tersebut harus aktif lebih lama dibandingkan siklus proses
pemanggil. Untuk skenario ini, CoroutineScope
eksternal harus digunakan seperti
yang dijelaskan dalam Postingan blog Coroutine & Pola untuk pekerjaan yang tidak boleh dibatalkan.
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
harus dibuat dan dikelola oleh class yang aktif lebih lama dari
layar saat ini. Ini dapat dikelola oleh class Application
atau
ViewModel
yang tercakup dalam grafik navigasi.
Memasukkan TestDispatchers dalam pengujian
Sebuah contoh dari
TestDispatcher
harus dimasukkan ke dalam class Anda dalam pengujian. Ada dua implementasi
yang tersedia di
library kotlinx-coroutines-test
:
StandardTestDispatcher
: Mengantrekan coroutine yang dimulai dengan scheduler, dan menjalankannya saat thread pengujian tidak sibuk. Anda dapat menangguhkan thread pengujian untuk mengizinkan coroutine lainnya yang diantrekan berjalan menggunakan metode sepertiadvanceUntilIdle
.UnconfinedTestDispatcher
: Menjalankan coroutine baru dengan segera, dengan cara memblokir. Hal ini umumnya mempermudah penulisan pengujian, tetapi memberi Anda lebih sedikit kontrol atas cara coroutine dijalankan selama pengujian.
Lihat dokumentasi setiap implementasi dispatcher untuk detail tambahan.
Untuk menguji coroutine, gunakan
runTest
pembuat coroutine. runTest
menggunakan
TestCoroutineScheduler
untuk melewati penundaan dalam pengujian dan
memungkinkan Anda mengontrol waktu virtual. Anda juga dapat
menggunakan scheduler ini untuk membuat dispatcher pengujian tambahan sesuai kebutuhan.
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()
}
}
Semua TestDispatchers
harus memiliki scheduler yang sama. Hal ini memungkinkan Anda
menjalankan semua kode coroutine pada satu thread pengujian agar pengujian
Anda menjadi deterministik. runTest
akan menunggu semua coroutine yang ada di scheduler yang sama
atau merupakan turunan dari coroutine pengujian yang telah selesai sebelum kembali.
Menghindari GlobalScope
Ini mirip dengan praktik terbaik Memasukkan Dispatcher. Dengan menggunakan
GlobalScope
,
meng-hardcode CoroutineScope
yang digunakan class
akan membawa beberapa kelemahan:
Mempromosikan nilai hard code. Jika Anda meng-hardcode
GlobalScope
, Anda juga mungkin meng-hardcodeDispatchers
.Membuat pengujian menjadi sangat sulit karena kode dieksekusi dalam cakupan yang tidak terkontrol, Anda tidak akan dapat mengontrol eksekusinya.
Anda tidak dapat memiliki
CoroutineContext
umum untuk dieksekusi bagi semua coroutine yang dibuat ke dalam cakupan itu sendiri.
Sebagai gantinya, sebaiknya masukkan CoroutineScope
untuk pekerjaan yang perlu aktif lebih lama
daripada cakupan saat ini. Lihat
Bagian membuat coroutine di lapisan bisnis dan data
untuk mempelajari topik ini lebih lanjut.
// 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
}
}
Pelajari GlobalScope
dan alternatifnya lebih lanjut di Postingan blog Coroutine & Pola untuk pekerjaan yang tidak boleh dibatalkan.
Membuat coroutine Anda dapat dibatalkan
Pembatalan di coroutine bersifat kooperatif, yang berarti bahwa ketika Job
coroutine
dibatalkan, coroutine tidak akan dibatalkan sampai terjadi penangguhan atau pemeriksaan
pembatalan. Jika Anda melakukan operasi pemblokiran di coroutine, pastikan
coroutine dapat dibatalkan.
Misalnya, jika Anda membaca beberapa file dari disk, sebelum mulai
membaca setiap file, periksa apakah coroutine dibatalkan. Salah satu cara untuk
pemeriksaan pembatalan adalah dengan memanggil
ensureActive
fungsi tersebut.
someScope.launch {
for(file in files) {
ensureActive() // Check for cancellation
readFile(file)
}
}
Semua fungsi penangguhan dari kotlinx.coroutines
seperti withContext
dan
delay
dapat dibatalkan. Jika coroutine Anda memanggil fungsi, Anda tidak perlu melakukan
pekerjaan tambahan.
Untuk informasi selengkapnya tentang pembatalan di coroutine, lihat Postingan blog pembatalan di coroutine.
Memperhatikan pengecualian
Pengecualian yang tidak ditangani yang ditampilkan dalam coroutine dapat membuat aplikasi Anda error. Jika pengecualian
cenderung terjadi, tarik pengecualian dalam isi coroutine yang dibuat dengan
viewModelScope
atau 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
}
}
}
}
Untuk informasi selengkapnya, lihat postingan blog Pengecualian di coroutine, atau Penanganan pengecualian coroutine dalam dokumentasi Kotlin.
Pelajari coroutine lebih lanjut
Untuk mendapatkan referensi coroutine lainnya, lihat halaman Referensi tambahan untuk coroutine dan alur Kotlin.