visible true

技術的なメモを書く

Jetpack Compose (0.1.0-dev04) でSeekBarをスクラッチする

Jetpack ComposeにはSeekBarがないので、必要な場合は今の所自分で作ることになります。で作りました。0.1.0-dev04 での実装なので将来そのままでは動かなくなると思うのでご注意ください。

f:id:sys1yagi:20200209145316p:plain
Preview

使う

実際の動作はこんな感じになります

streamable.com

実装

Draggableを使って実装します。横棒とか丸は頑張って描画してます。 Draggableは値の範囲がfixedなので、横幅が動的(いわゆるmatch_parent)の場合利用が難しいです。そのためDraw関数とparentSizeを使って、widthをstateに持つみたいなことをやってます。

@Composable
private fun paint(): Paint {
    return Paint().apply {
        color = MaterialTheme.colors().primary
        isAntiAlias = true
    }
}

@Composable
fun SeekBar(
    @FloatRange(from = 0.0, to = 1.0) progress: Float,
    fixedWidth: Dp? = null,
    onChangeProgress: (Float) -> Unit
) {
    val squareSize = 32.dp
    val barHeight = 8.dp
    val fixedWidthPx = withDensity(ambientDensity()) { fixedWidth?.toPx()?.value }
    val (width, setWidth) = state {
        fixedWidthPx ?: 0f
    }
    if (width == 0f) {
        Container(
            modifier = LayoutWidth.Fill
        ) {
            Draw { _, parentSize ->
                val newWidth = parentSize.width.value
                if (newWidth != width) {
                    setWidth(newWidth)
                }
            }
        }
    } else {
        val squareSizePx = withDensity(ambientDensity()) { squareSize.toPx().value }

        val max = width - squareSizePx
        val min = 0.dp
        val (minPx, maxPx) = withDensity(ambientDensity()) {
            min.toPx().value to max
        }
        val position = animatedDragValue(maxPx * progress, minPx, maxPx)
        val paint = paint()

        Draggable(
            dragDirection = DragDirection.Horizontal,
            dragValue = position,
            onDragValueChangeRequested = {
                position.animatedFloat.snapTo(it)
                onChangeProgress(position.value / max)
            }
        ) {
            Container(
                modifier = fixedWidth?.let { LayoutWidth(it) } ?: LayoutWidth.Fill,
                alignment = Alignment.CenterLeft,
                height = squareSize
            ) {
                Stack {
                    Padding(
                        top = squareSize / 2 - barHeight / 2,
                        left = squareSize / 2,
                        right = squareSize / 2
                    ) {
                        ColoredRect(
                            Color.LightGray,
                            height = barHeight
                        )
                    }
                    Draw { canvas, _ ->
                        canvas.drawCircle(
                            Offset(position.value + squareSizePx / 2, squareSizePx / 2),
                            squareSizePx / 2,
                            paint
                        )
                    }
                }
            }
        }
    }
}

おわりに

ProgressBarなども横幅fixedなんでスクラッチしたり、わりとスクラッチが必要だけど、結構カスタムなコンポーネント作るのそんなに難しくないので、UIライブラリがどんどん出てくるかもなと思ったりします。β、RCが待ち遠しいですね。

Jetpack Compose 0.1.0-dev03から0.1.0-dev04にしたときに変更が必要だったところ

Jetpack Compose 0.1.0-dev04が出ましたね、今回からリリースノートのページもできたみたいです。

developer.android.com

まだプレビューなんでどんどんAPIが変わっていきます。もしまともに使ってるとえらいことになるわけですが、個人的にproduction readyを待たずなんかアプリ出したろと思っているのでガンガン使っています。 で、0.1.0-dev03から0.1.0-dev04にしてみると案の定えらいことになったので変更が必要だったところをまとめます。

f:id:sys1yagi:20200131084046j:plain
0.1.0-dev03から0.1.0-dev04にしたときの様子

コンパイラの設定

0.1.0-dev03では、どうもkaptとの相性が悪く、Backend Internal error: Exception during code generation みたいなエラーがでてコンパイルできなかったのですが、0.1.0-dev04では、オプションを追加することで回避できるようになったようです。

android {
  composeOptions {
    kotlinCompilerExtensionVersion "0.1.0-dev04"
  }
}

最初、compileOptionsに書いてエラーになって頭を抱えたんですが、composeOptionsでした。

unaryPlusの廃止

+state とか +ambient とか +imageResource とかの、+が要らなくなりました。単純に+を消して回ればOK。

effectOfが廃止

unaryPlusの廃止と同時にeffectOfも廃止になりました。代わりに @Compose を使えとのこと。

before

private fun paint(color: Color, strokeCap: StrokeCap, strokeWidth: Dp) = effectOf<Paint> {
    val paint = +memo { Paint() }
    // ...
    paint
}

after

@Composable
private fun paint(color: Color, strokeCap: StrokeCap, strokeWidth: Dp): Paint {
    val paint = remember { Paint() }
    // ...
    return paint
}

まぁカスタムでeffectOf使うケースあんまりなさそうなのでもし引っかかったらという感じです。

memoがrememberにリネーム

before

val count = +memo { 0 }

after

val count = remember { 0 }

dp, sp, IntPxなどが移動

before

import androidx.ui.core.Dp
import androidx.ui.core.PxSize
import androidx.ui.core.dp
import androidx.ui.core.sp

after

import androidx.ui.unit.Dp
import androidx.ui.unit.PxSize
import androidx.ui.unit.dp
import androidx.ui.unit.sp

FlexRow, FlexColumnが非推奨

ここが一番たいへんでした。FlexRow、FlexColumnが非推奨となり、代わりにRow、Columnを使えとのこと。

次のようなレイアウトを考えると、

f:id:sys1yagi:20200131092353p:plain

以前はFlexRowとinflexible, flexibleを使って書いてました。

before

@Preview
@Composable
fun DefaultPreview() {
  MaterialTheme {
    FlexRow {
      inflexible {
        Padding(16.dp) {
          Row {
            Padding(left = 4.dp) {
              Text("1")
            }
            Padding(left = 4.dp) {
              Text("2")
            }
          }
        }
      }
      flexible(1f) {
        Container(
          modifier = ExpandedWidth,
          alignment = Alignment.TopRight
        ) {
          Padding(16.dp) {
            Text("こんにちは")
          }
        }
      }
    }
  }
}

FlexRowが非推奨となりinflexible, flexibleなども消滅しました。代わりにRowを使います。

after

@Preview
@Composable
fun DefaultPreview() {
  MaterialTheme {
    Row(modifier = LayoutWidth.Fill) {
      Padding(16.dp) {
        Row {
          Padding(left = 4.dp) {
            Text("1")
          }
          Padding(left = 4.dp) {
            Text("2")
          }
        }
      }
      Container(
        alignment = Alignment.TopRight,
        modifier = LayoutFlexible(1f)
      ) {
        Padding(16.dp) {
          Text("こんにちは")
        }
      }
    }
  }
}

Rowの中はデフォルトがinflexibleです。flexibleはLayoutFlexibleを使います。LayoutFlexibleRowScopeからしかアクセスできません。

ExpandHeight, ExpandWidthの廃止

LayoutHeight.Fill, LayoutWidth.Fillになりました。

おわり

どんどん進化してますね。βが出るのが楽しみです。ScrollingListというRecyclerViewぽいやつも早くほしいっす。

Jetpack Composeでカスタムフォントを使う

Jetpack Composeでカスタムフォントを使うには、FontFamily を用いる。

res/fontにフォントファイルを置き、FontFamilyにFontを渡す。

// res/font/ipam.ttfがあるとすると次のようになる。
FontFamily(
  Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal)
)

使う

実際使う際は次のように+memoなんかを使って取り出しておき、TextStyleにセットする。

@Preview
@Composable
fun CustomFontSample() {
    val fontFamily = +memo {
        FontFamily(
            Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal)
        )
    }
    Text(
        text = "こんにちは",
        style = TextStyle(
            fontSize = 14.sp,
            fontFamily = fontFamily
        )
    )
}

こうなる。

f:id:sys1yagi:20200118024058p:plain

ambientを用意する

使う箇所で毎度取り出すのは煩雑なのでambientを用意しておくとよさそう。

import androidx.compose.Ambient
import androidx.compose.Composable
import androidx.compose.memo
import androidx.compose.unaryPlus
import androidx.ui.text.font.Font
import androidx.ui.text.font.FontFamily
import androidx.ui.text.font.FontStyle
import androidx.ui.text.font.FontWeight

val IpamFontAmbient = Ambient.of<FontFamily>()

@Composable
fun IpamFontProvider(children : @Composable() () -> Unit) {
    val fontFamily = +memo {
        FontFamily(
            Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal)
        )
    }
    IpamFontAmbient.Provider(value = fontFamily, children = children)
}

次のようにIpamFontProviderのchildで+ambient関数を使ってProvideしているFontFamilyを取り出せるようになる。

@Preview
@Composable
fun CustomFontSample() {
    IpamFontProvider {
        val fontFamily = +ambient(IpamFontAmbient)
        Text(
            text = "こんにちは",
            style = TextStyle(
                fontSize = 14.sp,
                fontFamily = fontFamily
            )
        )
    }
}

setContent辺りの根本で囲んでおけばアプリ全体でどこでも取り出せるようになる。 このあたりはFlutterのproviderに考え方が似てるんじゃないかと思う。

おわりに

Compose面白い。

チームコラボレーションサービス「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

おわりに

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