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にあるサスペンド関数は全てキャンセル可能になっている

Kotlin Coroutinesをテストする

これは はてなエンジニア Advent Calendar 2019 13日目の記事です。

Kotlin Coroutinesのコルーチンをテストする時に必要なテクニックを紹介します。

コルーチンを同期的に実行したい

コルーチンを使って非同期処理をしているが、テストをする時には同期的に実行してほしいことはよくありますよね。

TestCoroutineScopeTestCoroutineDispatcher を使えば簡単にできます。

また、 runBlockingTest を使えばこれらのクラスが自動的に使われます。

kotlin.github.io

kotlin.github.io

CoroutineDispatcherを置き換える

普段使っているCoroutineDispatcherをテスト時に TestCoroutineDispatcher に置き換えるには下記のような方法があります

Dispatchers.Main

kotlinx-coroutines-test に含まれている Dispatchers.setMain / Dispatchers.resetMain を使いましょう

@Before
fun setUp() {
  Dispatchers.setMain(TestCoroutineDispatcher())
}

@After
fun tearDown() {
  DIspatchers.resetMain()
}

kotlin.github.io

kotlin.github.io

Dispatchers.Default / Dispatchers.IO 他

直接置き換える手段はないので、DIしましょう。

Flowに流れる値をテストする

Flow.collect をコルーチン内で実行すると値を全部受け取るまで処理がそこで止まります。

例えばRoomが返すような終わりのないFlowはキャンセルしない限り collect から先に進めないので、 launch で新しいコルーチンを立ち上げるとよいです。

その時 TestCoroutineScoperunBlockingTest を一緒に使ってやると Flow.collect が同期的に実行されます

@Test
fun test() = runBlockingTest {
  val flow = // create flow
  var actual = null
  val flowCollectJob = launch {
    flow.collect {
      actual = it
    }
  }

  // ここでFlowに値が流れそうなことを何かする

  // yield()でこのコルーチンがDispatcherを一旦手放すことでflow.collectに渡したblockが実行され値が反映される
  assertThat(actual).isEqualTo(expected)

  // 最後にキャンセルしてやらないと完了していないコルーチンがあるので例外が起きる
  flowCollectJob.cancel()
}
 

宣伝

2020年2月に五反田で開催されるDroidKaigi 2020でKotlin Coroutinesについて発表します。Kotlin Coroutinesの使い方に困ってる方は是非見に来てください!

これから始めるKotlin Coroutinesの導入

Kotlin Coroutinesが正式リリースされて1年が過ぎ、もはや私たちのAndroidアプリ開発には欠かせないものとなりました。
コルーチンはRxJavaのようなストリームやコールバック方式と比べて非同期的な処理を直感的に記述でき、これまでRxJavaを使っていたようなユースケースをある程度置き換えられるようになっています。
​
​
私はこの1年と少しでKotlin Coroutinesを複数のアプリに導入し既存のパラダイムの置き換えを進めてきました。
Kotlin Coroutinesの導入を行ったことでAndroidアプリ開発が楽になりましたが、大変になったこともありました。
恐らくKotlin Coroutinesが難しく感じて導入をためらっている方もいるでしょう。
​
このセッションでは、既存のAndroidアプリにKotlin Coroutinesを導入したことによってAndroidアプリ開発がどう変わったか、
Kotlin Coroutinesの導入を行ったことで困ったことをどう解決したかなどを紹介します。
皆様のKotlin Coroutinesの導入の参考になれば幸いです。
​
- RxJavaを使って書かれたコードを置き換える
- RxJavaを使った世界観にKotlin Coroutinesを混ぜる
- Retrofitを用いたHTTP通信をKotlin Coroutinesに対応
- RoomをKotlin Coroutinesで利用する
- Kotlin CoroutinesとView層との組み合わせ
- Kotlin CoroutinesとLiveDataの使い分け
- Kotlin CoroutinesをViewModelで活用する
- Kotlin Coroutinesとテスト
- 例外処理
- キャンセル可能に作る

droidkaigi.jp

明日の担当は id:polamjag です。

KotlinのprovideDelegate operatorについて

こんばんは、Kotlin大好きAndroidアプリエンジニアの id:takuji31 です。

※これは はてなエンジニア Advent Calendar 2018 21日目の記事です。

provideDelegate operatorについて

provideDelegate operatorはKotlin 1.1からあるオペレーターで、プロパティーの移譲オブジェクトを生成することができます。

Delegated Properties - Kotlin Programming Language

このオペレーターを挟むことで、Delegated property生成時にロジックを実行することができ、例えば以下のようなことができます。

  • プロパティーの親のオブジェクトに何か操作をする
  • プロパティーの生成のパラメーターを検証する

使い方

まずはプロパティーになるclassを定義しましょう。

読み書きできるプロパティーを作るには kotlin.properties.ReadWriteProperty を実装したclassが必要です。

ここではAndroidのSharedPreferencesを操作するStringのpropertyを定義します。

class SharedPreferencesProperty(private val key: String, private val defaultValue: String?) :
    ReadWriteProperty<PreferencesModel, String?> {
    override fun getValue(thisRef: PreferencesModel, property: KProperty<*>): String? {
        return thisRef.sharedPreferences.getString(key, defaultValue)
    }

    override fun setValue(thisRef: PreferencesModel, property: KProperty<*>, value: String?) {
        thisRef.sharedPreferences.edit().putString(key, value).apply()
    }
}

次に provideDelegate をもつclassを定義します。

SharedPreferencesに値を出し入れするにはキーを保持する必要がありますが、必ずしもキーがプロパティー名と同じではありません。

そこでプロパティーの移譲オブジェクトにキーを渡せるようにします。

もちろんキーは空文字だと困るので検証したいですね。

class SharedPreferencesPropertyProvider(
    private val key: String? = null,
    private val defaultValue: String? = null
) {
    operator fun provideDelegate(
        thisRef: PreferencesModel,
        prop: KProperty<*>
    ): ReadWriteProperty<PreferencesModel, String?> {
        val propertyName = prop.name
        val key = key ?: propertyName

        require(key.isNotEmpty()) { "Key cannot be empty" }

        return SharedPreferencesProperty(key, defaultValue)
    }
}

最後にこのプロパティーを使うclassを定義します。

open class PreferencesModel(context: Context, name: String) {
    internal val sharedPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)
    protected fun stringPreferences(key: String? = null, defaultValue: String? = null) = SharedPreferencesPropertyProvider(key, defaultValue)
}

class UserPreferences(context: Context) : PreferencesModel(context, "user") {
    var name: String? by stringPreferences()
    var profile: String? by stringPreferences("profile")
}

あとはこの UserPreferencesインスタンスのプロパティーをset/getするとSharedPreferencesを操作できます。

UserPreferences.profile にはキーの値を渡していますが、例えばこの値を空文字にするとインスタンス生成時に例外が発生するようになります。

正しくインスタンス生成ができるかはテストでチェックしてやるとよいでしょう。

最後に

provideDelegate メソッドを使うとDelegated propertyを更に柔軟にすることができるでしょう。

明日の担当は id:tkzwtks さんです。

Architecture Components ViewModelをAutoDisposeに対応させる。

※ これはtakuji31 Advent Calendar 2018、1日目(相当)の記事です。

adventar.org

ViewModel

ViewModelは我々Androidアプリエンジニアの生活になくてはならないコンポーネントの一つである。

AndroidのActivityのインスタンス単体のライフサイクルは特定の画面のライフサイクルより短く、画面回転やメモリー不足で簡単に消される。

だが通信などの非同期処理は画面を完全に抜け出すまでやっていてほしいし、データも保持されてほしく、そういう時にViewModelを使う。

使い方は公式ドキュメントを見てほしい。

developer.android.com

AutoDispose

AutoDisposeはUberが公開しているRxJava2のDisposableを自動的にdisposeするライブラリーである。

大半の処理は実行を指示したライフサイクルに対応するライフサイクルで処理をキャンセル(createならdestroy、resumeならpause)したいものである。

これをいい感じに自動的にやってくれるライブラリーがAutoDisposeだ。

1.0.0でandroidxにも対応していて安心。

github.com

使い方は簡単でRxJavaのObservableやSingleをsubscribeする前に

observable
  .autoDisposable(scope())
  .subscribe()

などとしてやるだけである。

これだけで現在の状態に対応したライフサイクルイベントが起きた時にdisposeされる。

ViewModelをAutoDisposeに対応させる

AutoDisposeには ScopeProvider というinterfaceがあり、これを使うと今のスコープをAutoDisposeに知らせることができる。

ScopeProvider#requestScopeCompletableSource を返せばこれが完了した時に Disposable がdisposeされる。

FragmentActivity といった LifecycleOwnerScopeProvider を継承した LifecycleScopeProvider (を実装した AndroidLifecycleScopeProvider) によってAndroidのActivityやFragmentのライフサイクルに応じたスコープを返すようになっている。

一方でViewModelはonClearedという単一のライフサイクルメソッドを持っている。

AutoDisposeで管理されるViewModel内のDisposableは全て onCleared が呼ばれた時に破棄されてほしい。

なので ScopeProvider を実装して onCleared が呼ばれた時に完了する CompletableSource を返すようにしてやればよい。

abstract class BaseViewModel : ViewModel(), ScopeProvider {
  private val onClearedSubject = CompletableSubject.create();

  override fun requestScope() : CompletableSource = onClearedSubject

  @CallSuper
  override fun onCleared() {
    onClearedSubject.onComplete()
  }
}

class HogeViewModel: BaseViewModel() {
  init {
    observable
      .autoDisposable(this)
      .subscribe()
  }
}

最後に

明日も何か書きます。

ThreeTen BackportのInstant.plus(long, TemporalUnit)は年を足せない

Androiderならきっとみんな大好きなはずのThreeTen Backportに関しての雑メモ。

今から100年後のInstantが欲しくなって Instant.now().plus(100, ChronoUnit.YEARS) ってやってみたけど UnsupportedTemporalTypeException で死んだ。

コードを読んでみると ChronoUnit.YEARS はサポートされていなくて以下の ChronoUnit しか使えなかった。

  • NANOS
  • MICROS
  • MILLIS
  • SECONDS
  • HOURS
  • HALF_DAYS
  • DAYS

月と年の計算は曖昧な部分(うるう年とか)があるからサポートできないのだろうか。

とりあえず厳格に100年後が欲しかったわけじゃないので plus(36500, ChronoUnit.DAYS) でなんとかすることにした。

問題のコードはこの辺り↓

https://github.com/ThreeTen/threetenbp/blob/9233702f6905c3b172f57fcda3fc278ad1542cac/src/main/java/org/threeten/bp/Instant.java#L710

RecyclerView.ItemDecorationでgetItemOffsetsの値を変えた時、レイアウトはどう動くか

雑なメモ

RecyclerView.ItemDecoration を使ってitemのoffset(bottom)を調整する機会があった。

ある位置を基準にしつつ、offsetを調整+スクロールして基準位置にあったitemが同じ位置に見えている必要があったのだが、なぜかズレる。

よく見てみたら見えている一番上のitemの位置が動かず、下にズレているのだ。

どうやら今見えている一番上のitemを基準にレイアウトが決定されるようだった。

仕方がないので基準位置より上にあるViewのoffsetを計算してその分だけスクロールすることにした。

lateinitの行儀の良い使い方

以前勉強会で「KotlinのlateinitDelegates.notNull()の使い分けがよく分からない」というお話をいただいた時に"基本的にはDelegates.notNull()を使うべきではないか?" と答えたしそれより前から最近もずっと思っていたのだが、(DroidKaigiの発表を聞くなどする感じ)どうも違うのではないかと思えてきた。

そのため、ここでlateinitの仕様について整理しつつ、そこから導き出される行儀の良い使い方を考察する。

結論

  • kaptを利用してJavaのコードからフィールドを操作したい場合はlateinitを使うべき
    • フィールドが露出すると不都合がある場合はDelegates.notNull()を使うべき
  • lateinitを普通に遅延初期化目的で使うならprivateで使うべき
  • 遅延初期化するが、タイミングは最初にアクセスされた時でよい&変更されないならlazyで十分
  • 小さなオブジェクトが大量に生成されると困るくらいパフォーマンスを気にする部分ではlateinitを使うとよい

lateinitとは

KotlinでNonNullなプロパティーの初期化をconstructorより後に遅らせられる機能。

元々はkaptで(例えば)Dagger2からインスタンスをプロパティーに直接代入できない、みたいなことを解決するアプローチとして追加された。

blog.jetbrains.com

lateinitの使い方

lateinit をプロパティー定義の前に書く。

lateinit var viewModel: HogeViewModel

fun onCreate(savedInstanceState: Bundle?) {
   // DI
   component.inject(this)

   viewModel.fuga // インスタンスにアクセスできる
}

制約

  • valには使えない(以前は使えたが今は使えない)
  • Nullableには使えない
  • プリミティブ型には使えない
  • 使うことでJavaからはそのプロパティーのfieldがプロパティーと同じvisibilityで見える
    • private 以外にすると外から見える可能性がある → 意図しない使い方になる可能性

Delegates.notNull() との違い

  • lateinitDelegateオブジェクトが作られないので、大量に使う場合にパフォーマンス的に有利
  • Delegates.notNull() はプリミティブ型にも使える

lazyとの違い

  • lazyは値をsetできない(1回限りの遅延初期化)
  • lazyはNullableでも使える

最後に

意外とlateinitもっとカジュアルに使ってもいいのかもしれない。