visible true

技術的なメモを書く

FlowのflowViaChannelを使ってRxBindingを置き換える

※こちらは古いので FlowのchannelFlowを使ってRxBindingを置き換える - visible true を参照ください。

Kotlin Coroutine 1.2.xでFlowというコールドストリームをサポートするクラスや関数群が登場しました。

Flow - kotlinx-coroutines-core

次のような感じでめっちゃRxJavaっぽい雰囲気ですが動作の仕組みはコルーチンでやってる感じです。

val f = flowOf(1, 2, 3) // Flowを固定値で作る
  .map {
    it * 2
  }
  .flowOn(Dispatchers.IO) // 実行コンテキストを設定できる

runBlocking {
  f.collect { // この呼出しで初めて値が送出され始める
    println(it)
  }
}

Channelはホットストリームなので取扱いが難しい的な話

本題と逸れるのであんまり語らないですが、こんな話があってFlowが登場しました。

Cold flows, hot channels

produceとかChannelだとObservable的な使い方できなかったり宣言時点で動き出すので管理大変やで〜みたいな感じですね。

例えばRxTextView#textChangesと置き換えてみる

FlowでRxTextView#textChangesのような機能を実装すると次のようになります。

fun TextView.textChangeAsFlow() =
  flowViaChannel<String?> { channel ->
    channel.offer(text.toString())
    val textWatcher = addTextChangedListener {
      channel.offer(it?.toString())
    }
    channel.invokeOnClose {
      removeTextChangedListener(textWatcher)
    }
  }

flowViaChannel関数を使ってChannelを使ったFlowが作れます。

次のように使います。

lifecycleScope.launchWhenCreated {
  binding.editNickName.textChangeAsFlow()
    .map { it?.isNotEmpty() ?: false } 
    .collect {
      binding.submitButton.isEnabled = it
    }
}

collect関数を呼び出した時に、呼び出し元のスコープでFlowが動作し始めます*1。 なので、呼び出し元のスコープがキャンセルされると、自動的にcollect関数も終了します。 例ではlifecycleScopeを使っているので、onStopの時にキャンセルされます。 もしFragmentで使う場合はviewLifecycleOwner.lifecycleScopeで呼び出すなどの工夫が必要そうです。

Flowを1 : nにしたい時はどうするといいかな?

TextViewにaddTextChangedListenerするケースだと、複数回呼び出しても問題ありませんが、 setOnClickListenerなどリスナーが上書きされる性質のものを使う場合は、1 : n の関係でFlowを作りたいですね。

broadcastIn関数を使ってBroadcastChannelを作ると 1 : n のFlowが作れます。

lifecycleScope.launchWhenCreated {
  val textChange = binding.editNickName
    .textChangeAsFlow()
    .broadcastIn(this)
    .asFlow()
  // ... 省略
}

broadcastIn関数は引数にCoroutineScopeを必要とします。BroadcastChannelを作った時点でhot streamになるので、スコープに所属させていないとリークしてしまうからです。broadcastIn関数を呼び出したあとasFlow関数で改めてFlowに変換しています。これでbroadcastIn関数の手前部分がhotになり、それ以降はcoldになります。

使い方は次のようになります。

lifecycleScope.launchWhenCreated {
  val textChange = editNickName
    .textChangeAsFlow()
    .broadcastIn(this)
    .asFlow()

  launch {
    textChange
      .map { it?.isNotEmpty() ?: false }
      .collect {
        sendButton.isEnabled = it
      }
  }
  launch {
    textChange
      .map { 140 - (it?.length ?: 0) }
      .collect { remain ->
        editNickNameLimit.text = "remain: $remain"
      }
  }
}

よさそう。

おわりに

Flow便利そうですね。RxBindingの機能は多岐に渡るので、今回のお試しだけで全部置き換えられるかっていうとわからないですが、概ねいけるんじゃないかなーと思います。

ところでFlowはまだpreviewなのでプロダクトへの投入は推奨されていません。 とはいえ大体うまく動くのでまぁちょっとくらいならいいんじゃないかなと思ったりしますが、 Structured Concurrencyの時のようにめちゃくちゃ変わる可能性もあるんでほどほどにしましょう。

IntelliJ IDEAからDocker上で動くSprint Bootアプリにdebuggerをつなぐ

IntelliJからdocker上で動いてるSpring Bootアプリにデバッガつなぎたいときってありますよね。 ドチャクソハマったのでメモします。

Dockerで動かすSpring Bootアプリでデバッグ用ソケットを起動する

gradleを使っている前提です。 gradleでSpring Bootを起動するタスクはbootRunです。 これに--debug-jvmオプションを付与すると、5005番でデバッグを受け付けてくれます。 docker-composeの設定イメージは次です。

version: '3'
services:
  app:
    image: openjdk:8
    container_name: TodoList
    ports:
      - 8080:8080
      - 5005:5005 # デバッグ用ポートも開けてあげる
    command: ./gradlew app:bootRun --debug-jvm # このオプションをつける

これでdocker-compse upするとサーバ起動前に5005番で待ち受けてくれます。

IntelliJのRemote Configurationで5005番に接続する

Edit ConfigurationでRemote Configurationを追加する。デフォルトで5005番なのでそのままでOK

f:id:sys1yagi:20190404163913p:plain

あとはつなぐだけ

f:id:sys1yagi:20190404164957p:plain

やったね。

handshake failed - connection prematurally closedが出る時は

localだとつながるのにDocker経由だとこんなエラーでてつながらない現象に苦しみました。

Error running 'debug': Unable to open debugger port (localhost:5005): java.io.IOException "handshake failed - connection prematurally closed"

原因はいろいろあるみたいですが、とりあえず自分の環境で起きた原因は、IntelliJの動作で使っているJVMと、Docker上で動いているJVMのバージョンが異なるからでした。 docker-composeで利用するimageをopenjdk:10からopenjdk:8にしたら繋げられました。 やったね。

おわりに

毎回5005番を待ち受けるのでsuspend=nみたいなオプション渡せるといいんだけどよくわからない。

KotlinTestのBehaviorSpecでGivenの単位でBeforeしたい

KotlinTestいいんですよね。 AndroidじゃなくてSpringBootで使ってるんですが。 Androidでも使えるかな?どうかな?まだ無理っぽい

beforeSpecはSpec開始時に一度だけしか実行されない

KotlinTestのSpec群はbeforeなんとか、afterなんとか系の関数を持っていて、オーバーライドして使うんですが、例えばbeforeSpec(spec: Spec)なんかだと、 Spec開始時に一度だけしか実行されないので、Given単位でDBの状態変えたいなって時に使えないんですよね。

beforeTestでisTopLevelを見る

別のタイミングとしてbeforeTest(testCase: TestCase)というのがあるんですが、こっちはGiven, When, Thenの全部で呼ばれる。これだとGivenでDB作ってもWhen行く時には消えてしまう。 beforeGivenとかないんかなと思ったけどないらしい。

beforeTestにはTestCaseが渡ってくる。TestCaseにいろんな情報があるので、これを使って判定すると良さそうってことでいろいろガチャガチャやってたら、どうもGivenはisTopLevelがtrueになるらしいということがわかった。

ということでbeforeTestでこんな感じでやると良さそう。

class TodoResourceSpec(
  val mockMvc: MockMvc,
  val dataSource: DataSource
) : BehaviorSpec() {

  override fun beforeTest(testCase: TestCase) {
    if (testCase.isTopLevel()) { // Givenの時だけtrueになる
      dbSetup(dataSource) {
        truncate("todo")
      }.launch()
    }
  }

  init {
    Given("I have empty todo list") { // ここと
       When("get todo list") {
         val result = mockMvc.perform(
             get("/api/todo")
               .contentType(MediaType.APPLICATION_JSON)
         )
         Then("should return empty list") {
           result.andExpect(status().isOk)
             .andExpect(content().json("""[]"""))
         }
      }
    }
    Given("I have a one todo") { // ここでtruncateが走る
        // 省略
    }
  } 
}

おわり

BehaviorSpecは抽象クラスなので、継承してbeforeGivenとか関数生やしてもいいかもしれないと思った。 TestCase#nameにはWhenとかThenとか文字列で書いてあるのでそれを見てbeforeWhenとかもできそう。 もっと違う方法あるきもする

Safe Args PluginでParcelable Arrayを使いたい時は型の末尾に[]って書けばいいらしい

ObjectArrayTypeとputParcelableArray

実装を読んでみるとObjectArrayTypeという型があり、putParcelableArraygetParcelableArrayマッピングしてるぽいことがわかる。

https://android.googlesource.com/platform/frameworks/support/+/9d3737306a6092c8ce8a98163b3a8070f0dcab7e/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/Types.kt#152

型の末尾の[]をチェック

https://android.googlesource.com/platform/frameworks/support/+/9d3737306a6092c8ce8a98163b3a8070f0dcab7e/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/Types.kt#47

あとここで、型の末尾に[]があるかチェックしている。

なのでこんな感じに書けばよい。

<argument
  android:name="authors"
  app:argType="jp.dip.sys1.aozora.models.AuthorCard[]" />

AuthorCardはもちろんParcelableでなければならない。

おわりに

うしさんありがとう。

Pass data between destinations  |  Android Developersには、

You can check Array to indicate that the argument should be an array of the selected Type value.

と書いてあるので、いずれAndroid Studio上でもarrayかどうかのチェックをUIで設定できるようになるはず(canaryだと動いてそう)。

リリース前レポート(Firebase Test Lab)で動作しているか判定する

リリース前レポート助かりますよね。Firebase Test Labベースでいろいろ根掘り葉掘りやってくれて最近ではフィードバックもいろいろ充実していて良い感じです

f:id:sys1yagi:20190218164210p:plain

リリース前レポートで動いてるのか判定したい

リリース前レポートいいんですけどロボットがガチャガチャ適当にやるので、あんまり掘ってほしくないルートに行ったりもするんですよ。

たとえば個人アプリでFirebase AuthでGoogleログインをサポートしてたりするんですが、Googleアカウント新規作成みたいなところまでいっちゃうことがよくあって最終的にクラッシュ扱いでレポートが上がってきたりします。

f:id:sys1yagi:20190218164908p:plain:w250
おい

また、広告表示なんかも抑えたいですね。実際に機械によるクリックを防止しろとドキュメントに書いてあったりします。リンクの先はなんか広告側で頑張ってブロックしてね的なドキュメントがあるんですが実施するのは難しい感じで厳しい感じでした。

f:id:sys1yagi:20190218165256p:plain

firebase.test.labフラグを見る

なんとかできないかな〜とつぶやいていたら2年前に自分で実装していたらしい。

完全に忘れてました。ということでFirebase Test Lab and Android Studio  |  FirebaseにあるようにFirebase Test Labの環境で動いているかを次のコードで判定できます。

fun isTestLab(context: Context): Boolean =
    Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"

やったね

おわりに

次は忘れないように書きました。ちばさんありがとう!

java.lang.ClassCastException: com.sun.tools.javac.code.Type$1 cannot be cast to javax.lang.model.type.DeclaredTypeが出たらテストで使うコードがエラーになってるぽい

AndroidX対応を進めていたら自分でつくって自分で使っているFragment生成周りが便利になるhttps://github.com/sys1yagi/fragment-creatorが適切にAndroidX対応できてないぽいことがわかり、いろいろと更新してたらテストで次のエラーがでてこけるようになってしまった。

java.lang.RuntimeException: java.lang.ClassCastException: com.sun.tools.javac.code.Type$1 cannot be cast to javax.lang.model.type.DeclaredType

これはアノテーションプロセッサで型チェックをしている場所で起こる。

https://github.com/sys1yagi/fragment-creator/blob/master/processor/src/main/java/com/sys1yagi/fragmentcreator/model/EnvParser.java#L49

compile-testingで読み込むJavaFileObjectがコンパイルできない場合に起こる

色々調べてみると、https://github.com/google/compile-testingで読み込むテスト用のJavaファイルをロードする時、Javaファイルで使ってるクラスを解決できない場合起こるぽいという事がわかった。設定をいじったから色々変になったのかな〜と思っていたのだけど、テストで使うコードのクラスパスが通ってない結果起こるエラーだとは...。

ということでテストで使うコード(たとえば https://github.com/sys1yagi/fragment-creator/blob/master/processor/src/test/assets/TypeSerializerFragment.java )を見るとandroidx.fragment.app.Fragmentを使うようになっていた。なるほどテストのクラスパスにandroidx.fragment:fragmentを通さないといけないらしい。

先人のコードを参考にする

JSR269周りでAndroidX対応を始めたタイミングとしては大分後発だと思うのできっと先人達がいい感じにしてるに違いないということでいくつかのリポジトリを渡り歩いた。

めちゃくちゃドンピシャだったのがPermissionsDispatcherで、ほぼ全体的に参考にさせてもらった。わいわい。 Replace deprecated "com.google.android" dependency with android.jar · permissions-dispatcher/PermissionsDispatcher@a5e0cac · GitHub

参考になったのは大きく2点

おわりに

AndroidXをサポートした Fragment Creator 2.0.0でました。

Kotlin Coroutine 1.1.0-alpha Change log 全部読む

本エントリは Kotlin Advent Calendar 2018 の記事です。

Kotlin Coroutine 1.1.0-alphaがリリースされましたね。「kotlinx.coroutines/CHANGES.md at master · Kotlin/kotlinx.coroutines · GitHub」Change logを読むことの重要さを思い知ったので全部読みます。

Major improvements in coroutines testing and debugging

コルーチンのテストとデバッグの改善のために新しいモジュールが2つ増えました。

kotlinx-coroutines-debugの追加

ひとつめはkotlinx-coroutines-debugkotlinx.coroutines/core/kotlinx-coroutines-debug at master · Kotlin/kotlinx.coroutines · GitHub

コルーチンの追跡とトレースができるようになります。ByteBuddyを使っているらしい。Androidで使えるかな..? dependenciesをtestImplementation に追加してねというのも気になる。

kotlinx-coroutines-testの追加

もう一つはkotlinx-coroutines-testkotlinx.coroutines/core/kotlinx-coroutines-test at master · Kotlin/kotlinx.coroutines · GitHub

Dispatchers.setMain関数などを提供してくれるらしい!これは助かる。

スタックトレースの改善

System.setProperty("kotlinx.coroutines.debug", "") ってセットしておくと、dispatcherを跨いでもスタックトレースで追えるようになる。これは助かる。

Basic stacktrace augmentation · Issue #493 · Kotlin/kotlinx.coroutines · GitHub

その他の改善

MainScopeファクトリとCoroutineScope.cancel関数を追加

https://github.com/Kotlin/kotlinx.coroutines/issues/829

MainScopeファクトリの中身がCoroutineScope(Dispatchers.Main + SupervisorJob()) で、root scopeではSupervisorJob使っていこうなってことなのかな〜うーんという感じ。CoroutineScope.cancelは便利そう。

resumeWithExceptioncancelのCancellableContinuationの競合が解決された。キャンセル中の例外は例外ハンドラに伝搬しなくなった。

特に感想なし

Dispatchers.DefaultJVM上のCPU消費量を削減

https://github.com/Kotlin/kotlinx.coroutines/issues/840

CoroutineSchedulerが処理がない時に余計に計算してたらしい。改善されてよかった

初期化されてないDispatcherをより素早く診断して検出

https://github.com/Kotlin/kotlinx.coroutines/issues/880

Dispatchers.Main使ってるのにandroid dependencies入れてないみたいな時にハングするケースがあったらしい。直ってよかった。普通に使ってる分にはあんまり関係なさそう。

Conflated channelが線形化可能になった

どういうこと〜?

コルーチンの結果の型がDisposableHandle だったときに起こる問題を修正

https://github.com/Kotlin/kotlinx.coroutines/issues/835

ハングっちゃうらしい。普通に使ってる分には関係なさそう。

Dispatchers. JavaFx の初期化のバグを修正

https://github.com/Kotlin/kotlinx.coroutines/issues/816

withTimeout関数のバグを修正

https://github.com/Kotlin/kotlinx.coroutines/issues/870

タイムアウトを負の数にすると、TimeoutCancellationExceptionじゃなくてCancellationExceptionが飛んでしまう問題を修正したらしい

Kotlin/Native でシングルスレッドワーカーをサポート

Kotlin/Nativeでもコルーチンが便利に使えるらしい。

JavascriptDispatchers.Defaultでjsdomをサポートした

あんまりよくわかってない

rxFlowableの型パラメータがAny で制限された

あんまりよくわかってない

Guava 27をサポート

kotlinx-coroutines-guava

コルーチンをプログレッシブモードでビルドするようになった

Kotlin 1.2.50とかで入ったモードっぽいけど詳細はわからない

ドキュメントをいろいろ修正

わいわい

感想

意外としれっといろいろ入るんだな〜。おもしろい。