Takuji->find;

株式会社はてなでアプリケーションエンジニアやってます、技術的な記事を書いているつもり

なんとなく理解するAndroidでKotlin Coroutinesを使う方法

この記事は社内のエンジニアが集まるScrapboxへ書いたページの転記です。

なんとなくKotlin Coroutinesについて理解が深まると幸いです。

参考URL

Coroutineを使うにあたって重要な要素

Suspending Function

CoroutineDispatcher

  • Coroutineの実行するスレッドを決める仕組み
  • RxでいうSchedulerみたいなもの
  • デフォルトでDsipatchersに3種類のDispatcherが用意されている
    • Main
      • メインのスレッドで実行
      • kotlinx-coroutines-androidを導入するとメインスレッドのHandlerにpostしてくれる
      • Dispatchers.Main.immediateを指定するとメインスレッドからの呼び出しなら即時実行してくれる
        • しない場合は次のイベントループで実行される
      • kotlinx-coroutines-testで別のDispatcherにdelegateできる
    • Default
      • デフォルトのDispatcher、親のCoroutineContextに特に何も設定していないとCoroutineの作成時にこれが使われる
      • JVM/Android上での実装はIOと共有で2以上、CPUのコア数か64のどちらか小さい値以下の共有スレッドプールで実行される
        • つまりスレッドが変わる可能性もあるので、スレッドセーフなデータの扱いを心がけた方が安全
    • IO
      • IO用のDispatcher
      • Defaultとスレッドプールが共有されるが、こちらはSystemのpropertyで数を制限できる
      • Defaultとの違いはAndroid上では名前くらいしかないのでDefault使えばいい気がしてる

CoroutineContext

  • Coroutineの情報を詰め込んでいるオブジェクト
  • 要素がある
    • CoroutineInterceptor
      • ほぼCoroutineDispatcher
    • ExceptionHandler
      • catchされなかった例外のハンドリングの仕組み
      • デフォルトだと何も処理されない
        • Androidの場合はAndroidExceptionPreHandlerが使われてスレッドに設定されているUncaughtExceptionHandlerに処理が以上される
          • →手でハンドリングしないとアプリがエラーで落ちる
      • https://kotlinlang.org/docs/reference/coroutines/exception-handling.html
    • Job
      • 現在のCoroutineのJob
      • 子のCoroutineを作る時はこのJobの子として作成される
      • cancelを呼ぶとキャンセルされる
        • 子のCoroutineも全てキャンセルされる
    • CoroutineName
      • Coroutineに名前をつける仕組み
      • デバッグの時に便利だろうけどあんまり使わない
    • その他色々あるけど実際に使うのはこれくらい
      • Coroutine内部では色々使われている模様

CoroutineScope

  • Coroutineを実行するためのスコープ
  • CoroutineContextをもつ
  • Coroutineを作成するためのエンドポイントになる拡張関数(コルーチンビルダーと呼ばれている)が用意されている
    • async
    • launch
  • スコープ内で生成されたCoroutineはスコープのCoroutineContextを継承する
    • Dispatchers.MainがDispatcherとして設定されていれば作成されたCoroutineはメインスレッド上で実行される
  • CoroutineScopeはネストできる
  • catchしていない例外やキャンセルが起きた時に並行実行されている他の処理がキャンセルされる単位

コルーチンを書く

1.CoroutineScopeを作る、既にあるものを使う

CoroutineScopeを作る

CoroutineScopeを作るには以下のような関数で行える

既にあるものを使う

既にあるものを使うと便利、よく使うCoroutineScopeは以下の通り

  • ViewModel.viewModelScope
    • ViewModelに用意されているCoroutineScope
    • SupervisorJob + Dispatchers.Main
      • 最近は Dispatchers.Main.immediate になった模様
    • ViewModel.onCleared 実行時にキャンセルされる
    • ViewModel内でコルーチンを作りたい時は基本的にこれを使うことになりそう
  • Lifecycle.coroutineScoope / LifecycleOwner.lifecycleScope
    • androidx.lifecycleLifecycle と連動しているCoroutineScope
    • androidx.lifecycle.Lifecycle.Event#ON_DESTROY のイベントを受け取った時にキャンセルされる
      • Activity/Fragmentだと onDestroy
      • Fragment.viewLifecyclerOwner だと Fragment.onDestroyView
    • launchWhenCreated / launchWhenResumed / launchWhenResumed といった便利コルーチンビルダーが生えている
  • GlobalScope
    • https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
    • デフォルトで用意されているやつ
    • 特定のJobに紐付かない
      • キャンセルはコルーチンそのものをキャンセルする必要がある
      • 別のScope内でGlobalScopeを使ってコルーチンを立ち上げてScopeをキャンセルしても立ち上げたコルーチンはキャンセルされない
    • あんまり使うべきじゃない
      • 公式ドキュメントにも使うのを強くおすすめしません、くいらいの温度感で書かれている
      • 例外として特定のSuspending Function内でDispatchers.Unconfined を使って produceなどのoperatorを使う時、に使うがこれもレアケースだと思う

2.Suspending Functionを書く

  • 特に難しいことはなくて suspend キーワードを使った関数を作ればよい
  • 基本的にSuspending Function内は同期的に実行される
    • 中で coroutineScopeasync を使って Deferred を立ち上げたりした場合は別
  • 中の処理をまるまる別のDispatcherで処理したい場合は withContext(defaultDispatcher) { /# do something #/ } みたいな感じで囲ってやるとよい

3. コルーチンビルダーでコルーチンを作る

  • 戻り値を受け取る必要がなければ launch
  • 値を受け取りたければ async
    • ただしawaitはSuspending FunctionなのでSuspending Function内で実行する必要がある
    • launch -> coroutineScope -> async みたいな囲うのが一般的
      • 例外を内部でcatchして値を返すのなら coroutineScope を使わないのもあり
  • 特定のContextで実行したければ withContext
    • 引数にCoroutineContextを受け取る
    • 一般的にCoroutineDispatcherをDispatchers.Defaultに切り替えて実行したい時に使われることが多い
    • NonCancelable を渡すとキャンセルされない
      • あまり使い道はなさそう

コルーチンを書く時に気を付けたいこと

例外処理

  • 例外処理は普通にtry/catchできる
  • ちゃんとcatchしないと親のコルーチンが死ぬ
    • SupervisorJobを使っていると死なない
    • CoroutineScope単位で死ぬので coroutineScope で囲う死なないという話はあるが、それでもcatchしないと普通に例外が吐かれてアプリが死ぬ

キャンセル可能な作りにする

  • コルーチンはキャンセルしてもコード側がキャンセル可能な作りになっていない限り処理が続行される
  • キャンセル可能に作っておくとアクセスされたら困るタイミングでViewにアクセスする、といった状況が避けやすい
  • よく使うキャンセル可能なコルーチンを作る関数
    • delay
      • 処理を中断して特定の時間後に再開することで後続の処理を遅らせる、コルーチンがキャンセルされたら再開されずにキャンセルされる
    • yield
      • その場でコルーチンを中断する、コルーチンがキャンセルされたら再開されずにキャンセルされる
      • Dispatcherが解放されるので、詰まってる別のコルーチンの処理を終わらせてから続きをする、といったことにも使える
  • kotlinx.coroutinesにあるサスペンド関数は全てキャンセル可能になっている