visible true

技術的なメモを書く

チームコラボレーションサービス「Miro」いいなぁという話

Ubie Advent Calendar 2019の9日目です。

チームコラボレーションツールっていっぱいありますよね。 UbieではSlackやメール、Notion、Google Hangouts、Jira、FigmaGithub、HERP、Salesforceなどのほかに「Miro」というチームコラボレーションサービスを使っています。

Miroってなに

Miroはオンラインのホワイトボードプラットフォームです。 もともとRealtime Boardっていうサービスでしたが、最近(といっても2019年の序盤)Miroって名前になったようです。

miro.com

Miroは、

  • ほぼ無限の2次元空間に
  • リアルタイムで同時に複数人で
  • 図を書く

ことができます。

図を作る際にテンプレートを選べますが、最初に配置されているアイテムが異なるだけで操作は同じです。

f:id:sys1yagi:20191209220158p:plain
Miroのテンプレート

UbieではMiroをどのように使っているか

複数のユーザが同時に図を書くサービスというと結構思いつきますよね。 FigmaGoogle図形描画、Cacooなどなど。

UbieではFigmaをUI/UXに関するデザインに利用し、それ以外の図は概ねMiroを使うといった形で運用しています。それ以外の図とはたとえば次の通りです。

  • 設計を議論するときに利用する図
    • アーキテクチャを図示する
    • ER図のようなものを書く
    • シーケンス図のようなものを書く
    • 責務を洗い出し境界を引く
    • etc..
  • タスクの洗い出しと分類
  • 画面遷移のバリエーションを洗い出す
  • 業務フローの図
  • 簡易ガントチャートの作成
  • ユーザーストーリーマッピング
  • 全社横断のKPT
  • 座席表
  • etc...

例: チームの計画をざっくり図に起こす

チームのバックログの管理はJiraを用いていますが、並んでいるタスクたちを時間軸に並べたとき厚みがどうなるかとかデッドラインがここだねとか、いつまでに何が終わってないとここはズレるよねといった話をするためにMiroで図を書いたりします。レトロスペクティブの際などにこれを眺めて色々組み替えたりします。

f:id:sys1yagi:20191209221503p:plain:w400

例: リリースフローを図にする

病院向けのプロダクトは、バグの混入やデグレなどをできるだけ防ぐために厚めのリリースフローになっています。リリース日が決まっており前日から準備するスタイルで、リリース担当を持ち回りで行います。リリースフローのドキュメントはありますが、リリース内容によって手順が異なる場合があったり、不具合が見つかった場合のフローなど複雑なので、図にもしつつ、認識わせしたり自動化可能なポイントを洗い出したりしています。

f:id:sys1yagi:20191209222212p:plain:w400

例: ユーザーストーリーマッピングを行う

チーム結成当初やクォータの区切り目などで、短期中期でフォーカスするものなどについて認識合わせしたり議論するために、ユーザーストーリーマッピングを行ったりしてます。スライスをざっくり置いてますがそこまで厳密に運用はせず、目線合わせを主な目的として使っています。 f:id:sys1yagi:20191210105754j:plain

例: 座席表を作る

座席表なんかも作ったりします。そろそろ40名を超えてきてかなり手狭になってきました... f:id:sys1yagi:20191209231032p:plain

ここがいいよMiro

ということで、Miroは概ね何にでも使えて便利です。特に強力だなと実感するのは、リモートで集まって議論する場合です。

リモートでもホワイトボードで議論してる感覚に近づける

さすがに物理ホワイトボードの体験にはかなわないんですが、リモートメンバーがいるミーティングをする場合などはMiroのほうが捗ります。

エンジニアが集まってミーティングする様子(わかりづらいけどリモートメンバーもいます)。

f:id:sys1yagi:20191209231926p:plain:w500

Ubieはその日にリモートするかは各自で決めるほか、フルリモートのメンバーもいたりします。ミーティングする際は大画面にGoogle Hangoutsを映して、画面共有しながら会話します。 設計議論のときなんかはリモートメンバーもMiroをいじりつつ会話しつつで概ね対面に近い成果物が得られます。また電子化された状態で残るのもいいですね。

ここが気になるMiro

複数人で図を書くという体験については申し分のないMiroですが、一点使っててしんどい部分があります。

  • 図の一覧画面で図のサムネイルが出ない(なんか設定する必要あるのかな?)
  • デフォルトでソート条件が last opened になっていて他者が作った新規の図が一生見つからない

f:id:sys1yagi:20191209234917p:plain

後者はまぁソート条件変えればいいんですが、デフォルトはlast modifiedとかにしてほしいですね...、前者についてもまぁ空間が無制限なのでどこを切り取るか難しいってのはありつつ基本ノープレビューは厳しい感じがあります。こうするといいよみたいな方法あったらぜひ教えてください。

おわりに

ざっくりですがMiroを紹介しました。Ubieに入社したときはすでにMiroが導入されていたので、Ubieでの業務的な前後の比較は僕はできないですが、前職含めてわりとホワイトボードと付箋最強でやっていたので初めて触ったときは結構衝撃を受けました。ぜひ一度試してみてください!

Ubieってどんな会社なんと思ったかたはWe-are-Ubie-会社・事業・組織・採用のことを是非御覧ください(やや画像重いかもです...)。

ノンアルコールビールめっちゃ増えてる

adventar.org

昨年のノンアルコールビールで晩酌すると風呂上がりにさっぱりプログラミングできて助かる - visible trueに引き続き、今年も書きます。まるで酔っ払っているかのように雑です。

自我を失ったノンアルコールビールたち

さて個人的にノンアルコールビールではアサヒドライゼロが好きで、いつもノンアルコール飲料を選ぶ時はドライゼロを買っています。

ある日、アサヒドライゼロを仕入れるか〜とスーパーに赴いたら新たなノンアルコールビールたちが棚にひしめき合っていました。

www.asahibeer.co.jp www.kirin.co.jp www.suntory.co.jp www.sapporobeer.jp

おいおいおいおいおい 「糖の吸収を抑える」、「脂肪を減らす!」 アルコールもプリン体も失い、更に糖の吸収を抑え脂肪を減らす機能を付与されるとは。 バーソロミュー・くまか。

飲んでみる

6缶入りはパッケージにでかでか「脂肪を減らす!」って書いてあってちょっと恥ずかしかったので1缶ずつ買いました。


「これちょっと恥ずかしい」

味はどれもおいしく、値段も手頃*1だし良い感じでした。この中ではカラダフリーが香りが好みで一番スキな感じでした。パッケージさえ気にならなければドライゼロと交互に飲もうと思うくらい。

ノンアルコール充実してきた

ビール系だけでも結構増えてるけど、日本酒系なんかも出てきてノンアルコールアツいですね。

www.gekkeikan.co.jp

酒税かからないから安いし、アルコール入ってないので健康的、美味しい。うれしい。

*1:120円/缶くらい

Ktor用のSpannerのスキーマバージョン管理ライブラリ「spanner-kase」を作った話

Ubie Advent Calendar 2019の2日目です。

最近チームで新しいサービスが必要になったのでKtorでやろうか!ってことでKtorでサービスを書き始めています。 データベースはGoogle Cloud Spannerを使おうということになりました。

Java/KotlinのWebアプリケーションにおいて、データベースのマイグレーションライブラリというとFlywayが有名かと思いますが、 残念ながらFlywayはGoogle Cloud Spannerをサポートしていません。

SpannerをサポートするPull Requestは存在するのですが、2018/01/01に作られたもので、Pull Requestを出した方はその後、google-cloud-spanner-jdbcを公式のgoogle-cloud-javaライブラリに追加*1したりしていますが、Flyway側の動きはなさそうです。

Pull Requestをつついたり引き継いだりしようか考えましたが、リリースタイミングをコントロールできない点と、機能的にKtorで使える範囲であれば小さそうということで、自分で作ってしまおうと考えました。

spanner-kase

名前はCloud SpannerのSchemaを入れておく場所ということでspanner-kase(スパナケース)にしました。

https://github.com/ubie-inc/spanner-kase

色々ケアレスミスをしてバージョンはいきなり1.1.3です。

spanner-kaseでできること

spanner-kaseでできることは次の通りです。

使い心地としては概ねFlywayかなと思います。Ktorで使うことだけを想定しているので、Spring Boot等ではうまく動かないかもしれません。

spanner-kaseの使い方

spanner-kaseを使うには次の手順が必要です

  • spanner-kaseをプロジェクトに追加する
  • マイグレーションファイルをresourcesに配置する
  • google-cloud-spannerのクライアントを初期化する
  • SpannerKaseDatabaseClient、MigrationDataScannerを作る
  • SpannerKaseを作ってマイグレーションを実行する

spanner-kaseをプロジェクトに追加する

spanner-kaseは内部でgoogle-cloud-spannerを使っていますが、推移的な依存関係を避けるためにimplementationで宣言しているので、利用時には別途google-cloud-spannerを追加する必要があります。

// build.gradle.kts
implementation("app.ubie.spanner-kase:1.1.3")
implementation("com.google.cloud:google-cloud-spanner:$GOOGLE_CLOUD_SPANNER_VERSION")

マイグレーションファイルをresourcesに配置する

Ktorプロジェクトを作ると最初からresourcesディレクトリがあると思うので、そこにマイグレーションファイルを置いていきます。パスは特に指定はないですが、ここではdb/migrationに配置しています。

.
├── build.gradle.kts
├── src
│   └── ...
├── resources
│   └── db
│       └── migration
│           ├── V1__User.sql
│           ├── V2__Todo.sql
│           └── V3__Permission.sql
...

マイグレーションファイルには命名規則があります。

V[VERSION]__[NAME].sql

VERSIONの範囲はLong*2です。

VERSIONが若い順に順次実行します。すでに実行済みのVERSIONは実行しません。VERSIONは年月日時分秒で書くのがおすすめです。

V20191201142511__User.sql

google-cloud-spannerのクライアントを初期化する

spanner-kaseは内部でgoogle-cloud-spannerを使っているので、まずはSpannerクライアントを作ります。

val options = SpannerOptions.newBuilder().build()
val projectId = options.projectId
val spanner = options.service

// spanner-kaseで使う
val instanceId = InstanceId.of(projectId, YOUR_INSTANCE_ID)
val databaseId = DatabaseId.of(projectId, instanceId.instance, YOUR_DATABSE_ID)
val databaseAdminClient = spanner.databaseAdminClient
val databaseClient = spanner.getDatabaseClient(databaseId)

databaseAdminClientはSpannerのDDL(Data Definition Language)を更新する際に、databaseClientはデータのCRUDを行う際に利用します。

SpannerKaseDatabaseClient、MigrationDataScannerを作る

次にSpannerKaseDatabaseClientMigrationDataScannerを作ります。

SpannerKaseDatabaseClientはspanner-kaseがバージョン管理のために使うテーブルの操作をする他に、マイグレーションファイルのSQLの実行などを行います。

val spannerKaseDatabaseClient = SpannerKaseDatabaseClient(
    instanceId.instance,
    databaseId.database,
    databaseAdminClient,
    databaseClient
)

MigrationDataScannerマイグレーションファイルの収集を受け持ちます。MigrationDataScanner自体はinterfaceなので、任意の実装を利用できます。予めClassLoaderMigrationDataScannerを用意しています。

KtorではApplicationを初期化する際に、environmentのClassLoaderを使うことで、resources内のマイグレーションファイルを利用できます。

@kotlin.jvm.JvmOverloads
fun Application.module() {
    // 省略
    val migrationDataScanner = ClassLoaderMigrationDataScanner(
        environment.classLoader, // io.ktor.application.Application.environment
        "db/migration" // relative path from resources dir
    )
}

SpannerKaseを作ってマイグレーションを実行する

あとはSpannerKaseを初期化して、migratie() を実行するだけです。

val configure = SpannerKase.Configure(
        spannerKaseDatabaseClient,
        migrationDataScanner
    ) 
SpannerKase(configure).migrate()

おわりに

早急に必要になる!と思ってザーッと作ったけど、優先度いくつか入れ替えてまだspanner-kaseを使うサービスはプロダクションでは出ていないので、まだもうちょいアップデートあるかもしれません。Ktor + Spannerは割とレアな気がしますが、もし機会があれば触ってみてください。

*1:FlywayでのサポートPR時に作ったものを公式に提案して取り入れられたっぽい

*2:1 - 9,223,372,036,854,775,807

Android Studio 4.0とJetpack Compose関連の見るとよいところなどのメモ

Youtube

Android Dev Summit '19のセッション! www.youtube.com

Android Studio 4.0 とJetpack Composeのセットアップ

Jetnewsというサンプルアプリを試したり、新規プロジェクトでJetpack Composeで始めたり、 既存のプロジェクトにJetpack Composeど導入する方法について書いてある。

developer.android.com

Jetpack Compose チュートリアル

Jetpack Composeの基本の解説。Composable functionsやレイアウト、スタイル、Themeなどの説明。 developer.android.com

Codelab

↑のドキュメントがCodelabになった感じ。わかりやすい。 codelabs.developers.google.com

Sample project

JetNewsというアプリのサンプル。 ナビゲーションドロワー、画面遷移などJetpack Composeで全て行っている。 データは埋め込みなので通信処理等はない。かなり参考になる。

github.com

f:id:sys1yagi:20191026143540p:plain:w300

Sample project2

自分で作ったもの。Navigation、ViewModelを使う。 Github APIをRetrofitで実際に検索する。 既存プロジェクトとの共存をイメージしているけど、JetNewsがSPAぽい世界観なので迷っていきている。

github.com

f:id:sys1yagi:20191026143505g:plain:w300

おわりに

まだまだ進化中という感じだけどいい感じなのではという気持ち。

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フォントをそのまま使えるのは良いですね~。