visible true

技術的なメモを書く

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:申請中です

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みたいなオプション渡せるといいんだけどよくわからない。