【Kotlin】Coroutineを理解したい!
非同期処理を行いたいときに、async/awaitのように書けないかなと思い調べたらKotlinはcoroutineが有効みたい。
viewModelScope
KTX拡張機能としてlifecycle-viewmodel-ktx
が用意されているので、coroutineを扱うのはviewModel
が良い。
スレッドをブロックしないためにも、実行を移動させ、新しいコルーチンを作成し、IOスレッドにて実行する
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
viewModelScope
: lifecycle-viewmodel-ktxで既に用意されているCoroutineScope
。このスコープ内で全てのコルーチンを実行する
Dispatchers.IO
: コルーチンがI/O処理用に予約されたスレッドで実行することを示す
Dispatchersとは
コルーチンの実行に使用するスレッドを決定している
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
3つのdispatcherがある。
Dispatchers.Main
- このディスパッチャを使用すると、コルーチンはメインの Android スレッドで実行されます。UI を操作して処理を手早く作業する場合にのみ使用します。たとえば、suspend 関数の呼び出し、Android UI フレームワーク オペレーションの実行、LiveData オブジェクトのアップデートを行う場合などです。
Dispatchers.IO
- このディスパッチャは、メインスレッドの外部でディスクまたはネットワークの I/O を実行する場合に適しています。たとえば、Room コンポーネントの使用、ファイルの読み書き、ネットワーク オペレーションの実行などです。
Dispatchers.Default
- このディスパッチャは、メインスレッドの外部で CPU 負荷の高い作業を実行する場合に適しています。ユースケースの例としては、リストの並べ替えや JSON の解析などがあります。
cancel scope
Androidではユーザーが違うActivity
やFragment
へ移動した時などに進行中の全てのcoroutineはキャンセルされる
On Android, you can use a scope to cancel all running coroutines when, for example, the user navigates away from an Activity or Fragment. Scopes also allow you to specify a default dispatcher. A dispatcher controls which thread runs a coroutine.
Coroutine Test
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
InstantTaskExecutorRule
は同期的にそれぞれのタスクをLiveDataに構成するJ Unitルール
MainCoroutineScopeRule
はコードベースのカスタムルール。テスト用の仮想を作成でき、単体テストでDispatchers.Mainを利用できるようになる
Coroutine Test Example
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1_000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
onMainViewClicked()
でコルーチンを開始、最初は0 tapsと表示する。
1秒後に1 tapsと表示されていればOKというテストの例
Coroutineのテストは仮想の時間で進んでいる
Coroutine /w Room, Retrofit
ともにsuspend
を付与するだけでmain-safeな関数になる。
なので、Dispatchers.Main
で呼んでも大丈夫。
※networkからデータをfetchしたり、データを書き込む処理があってもDispatchers.IO
は使ってはいけない!!!
Both Room and Retrofit use a custom dispatcher and do not use Dispatchers.IO.
Room will run coroutines using the default query and transaction Executor that's configured.
Retrofit will create a new Call object under the hood, and call enqueue on it to send the request asynchronously.
Room
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Retrofit
もしRetrofitのResultへフルにアクセスしたいなら、返り値の型をString
からResult<String>
に変えると良い。
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
suspend関数にしたことでRoomとRetrofitの処理をこのように変えることができる。
(main-safe
な関数はwithContext
は必要ない)
修正前:
suspend fun refreshTitle() {
// interact with *blocking* networking and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
titleDao.insertTitle(Title(result.body()!!))
} else {
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
修正後:
suspend fun refreshTitle() {
// interact with *blocking* networking and IO calls from a coroutine
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
throw TitleRefreshError("Unable to refresh title", cause)
}
}
すげ〜
Jetpack ComposeでCoroutineを使う
rememberCoroutineScope
を使って実装する
val composableScope = rememberCoroutineScope()
onClick = {
composableScope.launch {
viewModel.coroutineFun()
}
}
参考