visible true

技術的なメモを書く

Kotlin Fest 2019で「Kotlinコルーチンを理解しよう 2019」を話してきました

Kotlin Fest 2019楽しかったです。 今回は「Kotlinコルーチンを理解しよう 2019」を話してきました。 資料作りの様子、各セクションを作る時になに考えてたか、反省点などまとめます。

f:id:sys1yagi:20190826160412p:plain 前回と今回でロゴの形がちょっと変わってる

資料

コルーチンとはなにか、から実際使ってどうテストするかまでを45分でまとめるのはなかなか難しかったですが、一応網羅的にやれたか!?とは思っています。作る時はヒイヒイ言っててなにも考えてなかったけど改めてみると、これから触っていく場合に最初に読むものとして結構いいかもしれんと思うなどしました。 speakerdeck.com

コルーチンとはなにか、なにがうれしいのか

概念については自分のなかではある程度理解できてたものの、対象コルーチンや非対称コルーチンについて曖昧だったので改めて調べた。 新雑誌「n月刊ラムダノート」の『「コルーチン」とは何だったのか?』の草稿を公開します - まめめもはとても参考になりました、また、Google公式で紹介していた次のエントリも考え方の整頓にとても助かりました。

特にパート3のSingleRunnerの実装はこの辺に転がっているがめちゃんこ役に立つので参考にすると良いと思います。 このほか2004年の論文 Revisiting Coroutinesもまた改めて読んだ。前よりは理解できた。

コルーチンの何が嬉しいかについては昨年末のGoogle Play App Dojoでの登壇(http://sys1yagi.hatenablog.com/entry/2018/12/19/104023)でまとめていたのでそこまで迷いはなかった。

スライドの最初の、コンウェイによる概念の発明、simulaによる実装、様々な言語の様々な実装の話は、 発表では30秒くらいでサラッと流したけど、コンウェイの論文(http://www.melconway.com/Home/pdf/compiler.pdf)を見に行ったり、simulaのコルーチンプログラムを読んだり(http://staff.um.edu.mt/jskl1/talk.html の Chess game control using Two Masters approach.のセクション)、Module-2やSchemeLispに入門してみたり、Pythonのasync/awaitやRubyのFiberを試したりなんかしてめっちゃ時間をかけてしまった。が良かった。対象コルーチン、非対称コルーチン、各言語の実装、継続やスタックがどうとかいう議論など、歴史を感じる事ができた。これのおかげでサラッと話せたんだなと思う。

Kotlinにおけるコルーチンの仕組み

最初は結構細かく書いていた。suspendキーワードの話のあと、CPS(Continuation Passing Style)とステートマシン生成について話し(https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#implementation-details )、さらにsuspend関数がContinuationImplに、suspendラムダがSuspendLambdaに変換される様子をバイトコードを見て話したり、更にsuspendラムダの拡張関数を使ってコルーチンを実行する様子なんかも眺めたかったけど、実際使っていくにあたってはそこまで意識しないことだよなーっていうのと圧倒的に時間オーバーする事がわかって削った。

// suspendラムダの拡張関数。関数型に拡張関数生やせるの面白い
@InternalCoroutinesApi
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>) = runSafely(completion) {
    createCoroutineUnintercepted(completion).intercepted().resumeCancellable(Unit)
}

結果的に利用者にとって一貫性のある内容になったんじゃないかって気がしたので良かった(使う時にCPSが〜とかはあんま考えないので)。

Kotlinコルーチンのきほん

Kotlinコルーチンは関係要素が多すぎて何から説明するかとても大変だった。このセクションは基本的にKotlin公式のドキュメントに沿って話すことにした(https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html)。このセクションが一番長くて時間が足らないってことで結構削ってしまった。コルーチンビルダー関数について深ぼったり(特にsuspendラムダとか起動周りとか)、コルーチンスコープのインスタンス作って動かしてみる例があればもっと理解を深められただろうと思う。コルーチンディスパッチャーについても、こう使うと切り替えられますという感じで割り切ったのでちょっと心残りである。コルーチンディスパッチャーは奥が深くてDefaultとIOの違いとか、MainとServiceLoaderに関する話なんかも面白い。実行コンテキストの合成とかもややこしいけど面白い点だ。あともうちょい、このセクションに出る要素でほぼ実用はできるって点はもっと丁寧に説明できたらな〜と思ったりした。

コルーチンスコープと構造化された並行性

自分のなかで構造化された並行性についてはイメージがあったのでわりとスムーズにいった。最初はRoman氏のStructured concurrencyとそこから 参照されているNotes on structured concurrency, or: Go statement considered harmfulに触れたり、構造化プログラミングの非同期版やで〜みたいな話をしようと思ったけど、生まれた背景をそこまで細かく知る必要は無い気がしたので省略した。coroutineScope関数のあたりで、実はwithContext関数もスコープビルダーだよとか、ファストパスでwithContext関数呼び出しがネストしても実行コンテキストが変わらなければそのまま実行するよとか話したかったけど省略した。またSuperVisorについても省略した。viewModelScopeはSuperVisorJobを使ってたりするけど、まぁ通常はほぼ使わないと思うので省略。 分岐と合流の図は自分で書いてみてマジわかりやすいと思ってたので、わかりやすいという感想を見かけてやったねってなった。

コルーチンと設計

ここもまたサラリと書いた形になった。掘り下げようと思えば色々できたのだけど、考え方だけ伝えればいいかってことでだいぶ削った。そういえばスコープのモチベーションとリークの関係についてもっと説明するべきだった。その上でプラットフォームでスコープ提供してて最高って話ができると良かったかなと思う。suspend関数をメインセーフティで実装したり、コルーチンをメインセーフティで起動したりはまぁこんなものかな。メインセーフティでない例をしっかり示せてなかった気がするのでそこはちょっと反省点。

コルーチンのテスト

コルーチンのきほんのセクションと同等くらいの大きさでめっちゃ大変だった。 テスト方法についてもまだまだ固まってない部分もありどうまとめたもんか迷ったりした。 Githubにもぜんぜん実践的なテストコードが転がってなくて結構途方に暮れたが自分のアプリで徹底的にテスト書いて事なきを得た。間接的なコルーチンの呼び出しについて、ディスパッチャーを置き換える具体的な方法の紹介はあったほうがよかったなと思った。また、runBlockingTestやsetMainやRule周りも結構ザーッと書いてるんでもうちょい補助的な情報を示せたはずと思ったりした。準備の中でテストに関しての理解が間違ってた!みたいなことが数回発生して全練り直しを2回くらいして大変だった。TestCoroutineDispatcherとDelayHandler周りの実装は面白いので覗きたかったけどさすがに時間が足らないので諦めた。テスト周りは多分まだまだ書く度に悪戦苦闘することになりそう。

おわりに

今回準備にあたってコルーチンに関する論文をいくつか読んだ(ざくっとだけど)らとても面白かったのと、最近のCoroutines FlowでHoareのCSPモデルが関係する話( Kotlin Flows and Coroutines - Roman Elizarov - Medium)を読んだりして計算機科学をしっかり学んでみたいな〜大学院とかがいいのかな〜とかなんとか思ったりした。 しかしまずは車の免許を取ろうと思っている。そしてキャンプに行きたい。

参加者、スタッフの皆様ありがとうございました!引き続きKotlinを愛でていきましょう!

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

FlowのflowViaChannelを使ってRxBindingを置き換える - visible trueKotlin Coroutines 1.2.xでFlowというコールドストリームをサポートするクラスや関数群が登場しました。ってことでflowViaChannel関数について書いたら、Kotlin Coroutines 1.3.0-M1でflowViaChannel関数がdeprecatedになりました。previewなのでそういうこともあるでしょう。

ということで、FlowのflowViaChannelを使ってRxBindingを置き換える 改め、FlowのchannelFlowを使ってRxBindingを置き換える 話をします。

1.3.0-M1でなにが変わったか

Release 1.3.0-M1 · Kotlin/kotlinx.coroutines · GitHubによるとFlowにおけるコンテキスト保存の不変性について見直したとあります。コンテキスト保存の不変性というのは、Flowのブロックをどのコンテキストで実行するかを利用者がコントロールできることを保証するものです。

Flowのコンテキスト保存の不変性

たとえば次のコードはすべて同じスレッドで実行されます。

suspend fun a() {
  println("start: ${Thread.currentThread().id}")
  flow {
    println("emit: ${Thread.currentThread().id}")
    repeat(3) { emit(it) }
  }
    .collect {
      println("collect: ${Thread.currentThread().id}")
    }
}

出力は次のようになります。

start: 11
emit: 11
collect: 11
collect: 11
collect: 11

flowOn関数を使うと、upstreamの実行スレッドを変更できます。

suspend fun b() {
  println("start: ${Thread.currentThread().id}")
  flow {
    println("emit: ${Thread.currentThread().id}")
    repeat(3) { emit(it) }
  }
    .flowOn(Dispatchers.IO) // ここより上流はDispatchers.IOで動作する
    .collect {
      println("collect: ${Thread.currentThread().id}")
    }
}

出力は次のようになります。

start: 11
emit: 14
collect: 11
collect: 11
collect: 11

さて、次の場合はどうでしょうか*1

suspend fun c() {
  println("start: ${Thread.currentThread().id}")
  flow {
    kotlinx.coroutines.withContext(Dispatchers.IO) {
      println("emit: ${Thread.currentThread().id}")
      repeat(3) { emit(it) }
    }
  }
    .collect {
      println("collect: ${Thread.currentThread().id}")
    }
}

flow関数のブロック内で新たなコンテキストを使おうとしています。これだと利用者が例えばDispatchers.Mainで動作させたいと思ってもコントロールできませんよね。このコードを実行すると例外が投げられます。

start: 11
emit: 14

Flow invariant is violated: flow was collected in BlockingEventLoop@71cde863, but emission happened in LimitingDispatcher@73ad1220[dispatcher = DefaultDispatcher]. Please refer to 'flow' documentation or use 'flowOn' instead

このようにflowのブロックの実行コンテキストは、呼び出し元によってコントロールできることを保証するというのが、コンテキスト保存の不変性です。

Flowのコンテキスト保存の不変性の見直し

コンテキスト保存の不変性の見直しについては次のissueが詳しいです。 Flow context preserving property, thread safety and context changing · Issue #1210 · Kotlin/kotlinx.coroutines · GitHub

既存のコンテキスト保存の不変性では対応しきれない(あるいはコストがかかる)ユースケースがでてきたようです。 そのために新たに、異なるコルーチンからFlowCollector*2のemit関数の呼び出しを禁止するようになりました。 またchannelFlow関数を導入し、コンテキスト保存の不変性を保証しつつ、簡単に異なる実行コンテキストを利用できるようになりました。

その流れでflowViaChannel関数が非推奨になりました。非推奨になった明確な理由の説明がパッと見当たらなかったのですが、flowViaChannel関数のブロックがCoroutineScope.(channel: SendChannel<T>) -> Unitであるのに対して、channelFlow関数のブロックがsuspend ProducerScope<T>.() -> Unitであることから、ブロックが完了すると自動的にChannelをクローズするようにすることで、予期しない不具合を起こりづらくしてるのかな〜と思いました*3

channelFlowを使ってRxBindingを置き換える

ではflowViaChannel関数を使うのをやめてchannelFlow関数を使ってRxBindingを置き換えましょう

まずはbeforeのflowViaChannel関数を使うパターンです。

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

実はこれ1.3.0-M1では正しく動作しません。flowViaChannel関数の実装がchannelFlow関数を使う形に変更されたために、ブロックを抜けるとchannelがクローズされてしまうのです!怖いですね。

channelFlow関数を使った実装は次です。

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

channel.invokeOnCloseの代わりにawaitClose関数を使っています。これはProducerScopeに追加された拡張関数で、呼び出し元がcancelをするまで待ってくれます。便利ですね。

おわりに

Kotlin Coroutines 1.3.0-M1でFlowはPreviewからExperimentalになりました。まだ変更はいろいろありそうですが進捗してる感じがあってstableが楽しみですね。

*1:withContext関数をFQCNで書いているのは、FlowCollector.withContextの利用が禁止されていてコンパイルエラーになるためです。

*2:flow関数のブロックがsuspend FlowCollector.() -> Unitです

*3:普通に https://github.com/Kotlin/kotlinx.coroutines/pull/1214に書いてました..

Flutter for webでWebフォントを使う

使える気がするんだけど何か方法あるのかなーと思って試してみたら普通に使えばよいことがわかった。

index.htmlでWebフォントを読み込む

好きなフォントを書く

<html lang="en">
  <head>
    <!-- ...略 -->
    <link href="https://fonts.googleapis.com/css?family=Gentium+Book+Basic&display=swap" rel="stylesheet">
  </head>
</html>

fontFamilyで指定する

あとは普通にfontFamilyで名前を指定するだけ。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(
        widget.title,
        style: TextStyle(
            fontSize: 32,
            fontFamily: "Gentium Book Basic",
            fontWeight: FontWeight.bold),
      ),
    ),

こんな感じで反映される。

before after

まとめ

Webフォントをそのまま使えるのは良いですね~。

KtorでFirestore Local Emulatorにつなぐ

Firestore良さそうですよね。ローカルで動作を試せるLocal Emulatorがあるので気軽に試せてさらによさそうです。JVM環境で接続する例があんまり見当たらなかったのでメモします。

firestore local emulatorの準備をする

とりあえずインストールして、

firebase setup:emulators:firestore

次のコマンドで起動できればOKです。

firebase serve --only firestore

firebaseのバージョンとかでいろいろエラー出たりするのでググったりしてなんとか入れてください。

firestore local emulatorをポートを指定して起動する

firebase serve --only firestore ではポートが8080でしか起動できません、そこで次のコマンドでLocal Emulatorを起動します。

gcloud beta emulators firestore start --host-port=localhost:8812

次のような出力が得られれば成功です。

[firestore] API endpoint: http://localhost:8812
[firestore] If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
[firestore]
[firestore]    export FIRESTORE_EMULATOR_HOST=localhost:8812
[firestore]
[firestore] Dev App Server is now running.
[firestore]

application.confにlocal emulatorの設定を書く

Ktorからlocalhost:8812のLocal Emulatorに接続するために、application.confに設定を書きます。 特にfirebaseのための書き方などはないので、適当に書きます。

ktor {
  firebase {
    project_id = $PROJECT_ID
    firestore {
      emulator {
        host = localhost
        port = 8812
      }
    }
  }
}

Ktorから設定を読み込む

application.confの内容はApplication.environmentから読み込めます。

fun Application.module() {
  val firestoreEmulatorHost =
    environment.config.propertyOrNull("ktor.firebase.firestore.emulator.host")?.getString()
  val firestoreEmulatorPort =
    environment.config.propertyOrNull("ktor.firebase.firestore.emulator.port")?.getString()?.toInt()
  val projectId =
    environment.config.propertyOrNull("ktor.firebase.project_id")?.getString() ?: throw IllegalStateException(
      "firebase project id not found"
    )
}

Firestoreを初期化する

FirestoreでLocal Emulatorに接続する場合はFirestoreOptionsを使って直接Firestoreのインスタンスを作ります。

val firestore = FirestoreOptions
  .newBuilder()
  .setHost("$firestoreEmulatorHost:$firestoreEmulatorPort")
  .build()
  .service

本物につなぐ場合も考慮して次のようなobjectを作りました。

import com.google.auth.oauth2.GoogleCredentials
import com.google.cloud.firestore.Firestore
import com.google.cloud.firestore.FirestoreOptions
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.cloud.FirestoreClient

object FirebaseInitializer {
  data class FirestoreEmulator(val firestoreEmulatorHost: String, val firestoreEmulatorPort: Int) {
    fun toUrl() = "$firestoreEmulatorHost:$firestoreEmulatorPort"
  }

  fun firestore(
    projectId: String,
    emulator: FirestoreEmulator?
  ): Firestore {
    return if (emulator != null) {
      FirestoreOptions
        .newBuilder()
        .setHost(emulator.toUrl())
        .build()
        .service
    } else {
      val credentials = GoogleCredentials.getApplicationDefault()
      val options = FirebaseOptions.Builder()
        .setCredentials(credentials)
        .setProjectId(projectId)
        .build()
      FirebaseApp.initializeApp(options)

      FirestoreClient.getFirestore()
    }
  }
}

こんな感じでFirestoreを取り出せます。

val firestore = FirebaseInitializer.firestore(
  projectId,
  if (firestoreEmulatorHost != null && firestoreEmulatorPort != null) {
    FirebaseInitializer.FirestoreEmulator(firestoreEmulatorHost, firestoreEmulatorPort)
  } else {
    null
  }
)

やったね。

おまけ: KoinでInjection

せっかくなのでKoinでFirestoreインスタンスをinjectできるようにします。 とりあえずKoinをdependenciesに追加し、

dependencies {
  implementation "org.koin:koin-core:2.0.1"
  implementation "org.koin:koin-ktor:2.0.1"
}

モジュールを実装します。Applicationの拡張関数にしとくと楽です。

import io.ktor.application.Application
import org.koin.dsl.module

fun Application.applicationModule() = module {
  single {
    val firestoreEmulatorHost =
      environment.config.propertyOrNull("ktor.firebase.firestore.emulator.host")?.getString()
    val firestoreEmulatorPort =
      environment.config.propertyOrNull("ktor.firebase.firestore.emulator.port")?.getString()?.toInt()
    val projectId =
      environment.config.propertyOrNull("ktor.firebase.project_id")?.getString() ?: throw IllegalStateException(
        "firebase project id not found"
      )
    FirebaseInitializer.firestore(
      projectId,
      if (firestoreEmulatorHost != null && firestoreEmulatorPort != null) {
        FirebaseInitializer.FirestoreEmulator(firestoreEmulatorHost, firestoreEmulatorPort)
      } else {
        null
      }
    )
  }
}

あとはアプリケーションの初期化時にKoinをinstallしてモジュールを設定すれば、適当に取り出せるようになります。

fun Application.module() {
  install(Koin) {
    modules(
      applicationModule()
    )
  }
  // ...
  routing {
    get("/firestore") {
      val firestore: Firestore by this@routing.inject()
        // do something
    }
  //...
}

まとめ

Firestore Local Emulator良さそう。admin的な画面って内蔵されてないんだろうか?ある気がする。わからない。

Kotlin Fest 2019のセッションに応募しました。

今年もKotlin Festが開かれますね。もうセッションの応募はされたでしょうか? トラック数が増えたので今年はさらにいろんなジャンルが聞けるのではないかとワクワクしています。Kotlin/JSとかKotlin/Native, MPPが特に気になります。

kotlin.connpass.com

とりあえず4つほど応募しました。応募内容は次のとおりです。 他の方々の応募状況も見てみたいっす

言語機能

Kotlin コルーチンを 理解しよう 2019

概要

Kotlin コルーチンは2017年3月にexperimentalな機能としてKotlin 1.1とともに登場しました。 その後2018年10月に正式版の1.0.0がリリースされるまでに、様々な変更がありました。 特に2018年9月に加えられた並行性の構造化に関する変更は、それ以前のコルーチンのコードが利用できなくなるなど、とても影響が大きいものでした。ある時点より以前の情報が誤りになってしまう状況は、学習者にとって混乱のもととなります。

本セッションではKotlinコルーチンの簡単な歴史をおさらいしつつ、コルーチンとはなにか、コルーチンの簡単な利用方法、コルーチンスコープと並行性の構造化がなぜ生まれたのか、アプリケーションでコルーチンを利用する時の考え方、コルーチンのテスト方法などについて解説します。本セッションがKotlin コルーチンの適切な知識へのガイドになれば幸いです。

Kotlin コルーチン 「Flow」を味わい尽くす

概要

Kotlin コルーチンといえば「async/await」がよく注目されますが、それ以外にもいくつかの機能があります。 Channelは、Kotlin1.1においてコルーチンをexperimentalでリリースした時から提供している機能のひとつです。 async/awaitは単一の値をやりとりしますが、Channelはストリームを取り扱います。

Channelは連続的な値を非同期に送受信する用途に便利ですが、ホットストリームである点が利用の幅を制限していました。 Koltin コルーチン 1.2.0から「Flow」というコールドストリームがpreview版として追加されました。 本セッションでは、ホットストリームとコールドストリームとはなにか、ホットストリームの課題、Flowの仕組み、Flowの現在の使い方、Flowの使い所などについて解説します。

Android Kotlin

Design of Android Application with Jetpack and Coroutines

概要

Google I/O 2019でKotlinがAndroidアプリケーション開発の第1言語に躍り出ました :tada: 。 Android Jetpackの各種ライブラリではKotlin向けの拡張(ktx)が充実し、また非同期処理を必要とする部分ではコルーチンのサポートが相次いで行われており、Android Jetpack + Kotlin + コルーチンでアプリケーションを作る下地が整いつつあります。

本セッションでは、Google I/O 2019で発表されたAndroid JetpackのLifecylce、LiveData、ViewModelのコルーチンに関する機能を説明し、これらを用いたときのアプリケーション設計をどうするべきかや、テストをどのように行うべきかなどについて解説します。

Server side Kotlin

Ktorで小さく始めるAPIサーバ

概要

2018年11月にJetbrain謹製のウェブアプリケーションフレームワークであるKtor 1.0がリリースされました。 Ktorはサーバのコアコンポーネントに対して、実行エンジン(TomcatやJettyなど)、認証、ロギング、CORS、Routingなど必要な機能をDSLで宣言的に追加していくスタイルを採用しています。 必要最小限の機能で始められるので、プロトタイピングや単一の責務を持つアプリケーションを素早く作るのに適しています。

本セッションではKtorのコンセプト、提供されている機能群、基本的な使い方を説明し、簡単なTODOアプリケーションのREST APIサーバを作ります。Ktorで小さく始めるAPIサーバを体験しましょう!

まとめ

どれか引っかかるといいなー。 どしどし応募しましょう!!

loco-core 1.1.0をリリースしました。

Android向けロギングライブラリ「Loco」をリリースしました。 - visible true で1.0.0をリリースしたのですが速攻いくつか修正を入れて1.1.0をリリースしました。 しれっと1.0.1も出してました。 あとjcenterにも登録できたのでサクッと導入できるようになりました。

GitHub - sys1yagi/loco: loco (Log Coroutine) is a logging library using coroutine for Android.

loco-core-1.0.1

修正は2点。どちらも@chibatchingさんが直してくれました。ありがとうございます!

Release core-1.0.1 · sys1yagi/loco · GitHub

  • [Bug fix] Close channel on runner stopped #2 @chibatching 🍰
    • LocoRunner内のChannel閉じ忘れを修正
  • [Cleanup] Add const modifier #1 @chibatching 😄
    • 定数にconst付け忘れを修正

loco-core-1.1.0

Release core-1.1.0 · sys1yagi/loco · GitHub

  • [Breaking Change] Change LocoConfig interface #16

破壊的変更です。LocoConfigの引数をいくつか変更しました。 SenderとLocoLogのMappingをなくして、sendersを渡す時点で設定できるようにしました。 LocoConfig.Extraを追加し、必須でないパラメータはExtraに置くようにしました。

Loco.start(
  LocoConfig(
    store = InMemoryStore(), 
    smasher = GsonSmasher(Gson()), 
    senders = mapOf(
      // log senders and mapping
      StdOutSender() to listOf(
        ClickLog::class
      )
    ),
    scheduler = IntervalSendingScheduler(5000L),
    extra = LocoConfig.Extra(
      sendingBulkSize = 30
    )
  )
)
  • [New feature] implement default sender #12

デフォルトのSenderを設定できるようにしました。Senderが1つの場合はマッピング設定は不要になります。

Loco.start(
  LocoConfig(
    store = //... ,
    smasher = //... ,
    senders = mapOf(),
    scheduler = //... ,
    extra = LocoConfig.Extra(
      defaultSender = LocatSender()
    )
  )
)

今後

今後はモジュール周りの追加を中心に更新していきます。 RoomによるStoreとか、KoshiによるSmasherとか、ExponentialBackoffなSendingSchedulerとか、ListをJsonArrayに予め変換するSenderなどなど。 こんなモジュールもほしいな〜っていうのがあればissue作ってくれると嬉しいです。

Issues · sys1yagi/loco · GitHub

Android向けロギングライブラリ「Loco」をリリースしました。

AndroidアプリでロギングするといえばPureeかなと思います。 かなり安定しているしいい感じに動く。 ただコードベースがJavaなので、たま〜に不具合でた時などに追っかけるのが結構たいへんだったり、 Gsonに依存しているので、別のJsonライブラリ使ってる場合ちょっとな〜ってなったりすることがあります。

かねてから何らかの形で書き直しできないかな〜と思いつつ時間が取れなかったんですが、

ということでついに手を出してみたところ、結構スッと出来上がったのでリリースします。 LocoはLog Coroutineの略です。

https://github.com/sys1yagi/loco

構造

Locoは次のような構造を持ちます。

  • Smasher: ログをシリアライズする // ここだけ料理つながりな命名。ただ直感的ではない気がするので追々Serializerに変わるかも
  • Store: シリアライズしたログを一旦永続化する
  • Sender: シリアライズしたログを送信する
  • SendingScheduler: 送信間隔などを決める

それぞれInterfaceなので好きな実装ができます。

f:id:sys1yagi:20190519185303p:plain

セットアップ

まだjcenterに公開されてない*1のでbintrayのrepositoryをrootのbuild.gradleに追加する必要があります。

allprojects {
  repositories {
    maven { url "https://dl.bintray.com/sys1yagi/maven" }
  }
}

あとはdependenciesに必要なものを追加するだけです。 予めAndroidで使えるSmasherとStoreを用意してあります。

dependencies {
  // core
  implementation 'com.sys1yagi.loco:loco-core:1.0.0'
  
  // Gsonでシリアライズする。filterを追加して加工ができる  
  implementation 'com.sys1yagi.loco:loco-smasher-filterable-gson:1.0.0'

  // SQLiteでログを保存する
  implementation 'com.sys1yagi.loco:loco-store-android-sqlite:1.0.0'
}

この他にも便利モジュールができたら随時追加していきます。汎用性がありそうなものはPRもらえると嬉しいです。

使う

詳しくはsampleを見ていただきたいですが、概ね次のような感じでセットアップします。

class SampleApplication : Application() {
  override fun onCreate() {
    Loco.start(
      LocoConfig(
        store = LocoAndroidSqliteStore(), // loco-store-android-sqlite
        smasher = FilterableGsonSmasher(Gson()), // loco-smasher-filterable-gson
        senders = NetworkSender(), 
        scheduler = IntervalSendingScheduler(5000L) // 5000msおきに送信する
      ) {
            // SenderとLocoLogをマッピングする
            logToSender[NetworkSender::class] = listOf(
              ClickLog::class, // LocoLogを実装したクラスたち
              ScreenLog::class
           )
      }
    )
  }
}

// SendingSchedulerは今の所自前で用意しとく必要があります
class IntervalSendingScheduler(val interval: Long) : SendingScheduler {
  override suspend fun schedule(
    latestResults: List<Pair<Sender, SendingResult>>,
    config: LocoConfig,
    offer: () -> Unit
  ) {
    delay(interval)
    offer()
  }
}

data class ClickLog(
    val value: String
) : LocoLog

data class ScreenLog(
    val screenName: String
) : LocoLog

あとはどこからでもLogを送信できます。

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Loco.send(ScreenLog(this::class.java.simpleName))

    setContentView(R.layout.activity_main)
    // ...
  }
}

終わりに

まだ1.0.0で機能が不足してたり不具合あるかもしれないのでいろいろ触ってみてissue作ったりPRもらえると嬉しいです。

https://github.com/sys1yagi/loco

*1:申請中です