Takuji->find;

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

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もっとカジュアルに使ってもいいのかもしれない。

KotlinのSealed Classを使いこなす

こんにちは、三度の飯よりKotlinが好きな id:takuji31 です。

※ これは、はてなエンジニア Advent Calendar 2017 7日目の記事です。6日目は id:hayajo_77 さんの「Webオペレーションエンジニアとし研修して1ヶ月経ちました」でした。

hayajo.hatenablog.jp

今日は私が愛してやまないKotlinのSealed Classの使い方について紹介します。

Sealed Classについて

Sealed Classとは継承を制限して、階層を明確にできるクラスです。

詳しくは公式ドキュメントを参照してください。

Sealed Classes - Kotlin Programming Language

もう少し分かりやすく言うと、1つのファイル内でだけ継承できるクラスです。

Sealed Classを使う利点

普通の(final or open)クラスと比べて

外部からの継承を制限できるので、コンパイル時に子クラスの階層が全て明らかになります。

これで何ができるかというと、when式とis演算子の組み合わせで網羅性のチェックができます。

例えば下のコードのように、通常の A を継承する B/C/D というクラスがある時に

open class A
class B : A()
class C : A()
class D : A()

このAの子クラスのインスタンスがどのクラスか判定して何か値を返す関数は

fun isSomething(obj : A) : Boolean {
  return when (obj) {
    is B -> true
    is C -> true
    is D -> false
  }
}

と書くとコンパイルエラーになる(B/C/D以外の可能性がないことを判別できない)ので、以下のように書くと思います。

fun isSomething(obj : A) : Boolean {
  return when (obj) {
    is B -> true
    is C -> true
    is D -> false
    else -> throw RuntimeException("Not reached!!!!")
  }
}

elseは(自分達が書いているコード的には)絶対通らないはずだけど、例外を投げるか何らかの値を返す必要があります。

もちろんこのコードがライブラリーにあったとして、利用者がAを継承したEというクラスを作って渡すと、このコードはelseに到達しますね。

Sealed Classにすることで、コンパイラーがこのAの子クラスはB/C/Dしかないことを知られるので、最初のコードでも問題なくなります。

こうすることで、後で自分でAを継承する別のクラスを作ると、ここに選択肢を追加しないとコンパイルが通らなくなり、処理の追加漏れがなくなります。

Enumと比べて

前の項で書いている内容は、Enumでも実現できますね。

Enumと比べてSealed Classを利用する利点は、「Sealed Classを継承するのはobjectでもclassでもよい」ということではないでしょうか。

Sealed Classの子クラスそれぞれをEnumの値のように扱うことで、「Enumの各値に可変の値を持たせるような仕組み」ができます。

SwiftだとAssociated Valueと呼ばれるものですね。

使い方

使い方は簡単で、class宣言の前にsealedと書き、子クラスを全て同じファイル内で宣言するだけです。

sealed class A
class B : A()
class C : A()
class D : A()
object Z : A()

こうすることで、クラスAはこのファイル内でしか継承できなくなります。

また、Aはabstractなクラスかつ、コンストラクターがprivateになるので、直接インスタンス生成はできなくなります。

あとは通常のクラスと同じように使います。

どういう使い方をしている?

私が実際にSealed Classを使うシチュエーションを紹介します。

決まった文字列(全てが定数ではない)を受け取りたい時

これはSealed Classの「ファイル外で継承できない」仕組みを活用する例です。

例えばAndroidアプリでGoogle Analyticsにスクリーン名を送りたい時、スクリーンは定められた値なので、大半が定数で定義可能です。

しかし、完全な定数で済めばいいのですが、ページングをしている時にパラメーターとしてページ数を送りたいかもしれません。

また、普通にStringを送る運用だとミスって意味の分からない文字列を送ってしまうかもしれません。

こういった場合にそれぞれのスクリーン用にSealed Classを継承したobjectやclassを作り、Google Analyticsの送信用のラッパーを用意して、そこで値を分解して送るとこの問題を防ぐことができます。

sealed class GAScreen() {
  abstract val screenName: String

  open class Simple(override val screenName: String) : GAScreen()

  object Top : Simple("top")
  object Settings : Simple("settings")
  object List(private val page: Int) : GAScreen() {
    override val screenName: String = "list[page=$page]"
  }
}

ラッパー側は以下のようになります。

class GoogleAnalyticsWrapper() {
  fun sendScreen(screen: GAScreen) {
    val screenName = screen.screenName
    // ここでスクリーン名を送る
  }
}

こうすることで、うっかり変な文字列を送ることがなくなります。

色々な値を受け取って表示するActivityを起動するIntentに渡すパラメーターオブジェクト

こちらは"すごいEnum"っぽく使う例ですね。

Intentに複数のパラメーターをセットしたい時、全部を別々に手で受け取るのは面倒ですよね。

そのパラメーターの中にNullableなものがあって、別のパラメーターの値によって値が入ってなかったりすると思います。

こういう時にパラメーターオブジェクトを作って、Parcelableにして渡すのが有効と考えられます。

sealed class MainScreen() {
  object Top : MainScreen()
  object Settings : MainScreen()
  class Feed(val skipTutorial: Boolean) : MainScreen()
  class Category(val category: Category) : MainScreen()
  class EntryList(val initialTab: Tab) : MainScreen()
}

これをActivity側で受け取ってwhen式を使うなどして処理してやります。

when(screen) {
  is Top -> {
    openTop()
  }
  is Settings -> {
    openSettings()
  }
  is Feed -> {
    openFeed(screen.skipTutorial)
  }
  is Category -> {
    openCategory(screen.category)
  }
  is EntryList -> {
    openEntryList(screen.initialTab)
  }
}

最後に

KotlinのSealed Classを使って堅牢な設計のアプリを作りましょう。

明日の担当は id:aereal さんです!