Jetpack Compose (0.1.0-dev04) でSeekBarをスクラッチする
Jetpack ComposeにはSeekBarがないので、必要な場合は今の所自分で作ることになります。で作りました。0.1.0-dev04
での実装なので将来そのままでは動かなくなると思うのでご注意ください。
使う
実際の動作はこんな感じになります
実装
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が出ましたね、今回からリリースノートのページもできたみたいです。
まだプレビューなんでどんどんAPIが変わっていきます。もしまともに使ってるとえらいことになるわけですが、個人的にproduction readyを待たずなんかアプリ出したろと思っているのでガンガン使っています。 で、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を使えとのこと。
次のようなレイアウトを考えると、
以前は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
を使います。LayoutFlexible
はRowScope
からしかアクセスできません。
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 ) ) }
こうなる。
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、Figma、Github、HERP、Salesforceなどのほかに「Miro」というチームコラボレーションサービスを使っています。
Miroってなに
Miroはオンラインのホワイトボードプラットフォームです。 もともとRealtime Boardっていうサービスでしたが、最近(といっても2019年の序盤)Miroって名前になったようです。
Miroは、
- ほぼ無限の2次元空間に
- リアルタイムで同時に複数人で
- 図を書く
ことができます。
図を作る際にテンプレートを選べますが、最初に配置されているアイテムが異なるだけで操作は同じです。
UbieではMiroをどのように使っているか
複数のユーザが同時に図を書くサービスというと結構思いつきますよね。 Figma、Google図形描画、Cacooなどなど。
UbieではFigmaをUI/UXに関するデザインに利用し、それ以外の図は概ねMiroを使うといった形で運用しています。それ以外の図とはたとえば次の通りです。
- 設計を議論するときに利用する図
- アーキテクチャを図示する
- ER図のようなものを書く
- シーケンス図のようなものを書く
- 責務を洗い出し境界を引く
- etc..
- タスクの洗い出しと分類
- 画面遷移のバリエーションを洗い出す
- 業務フローの図
- 簡易ガントチャートの作成
- ユーザーストーリーマッピング
- 全社横断のKPT
- 座席表
- etc...
例: チームの計画をざっくり図に起こす
チームのバックログの管理はJiraを用いていますが、並んでいるタスクたちを時間軸に並べたとき厚みがどうなるかとかデッドラインがここだねとか、いつまでに何が終わってないとここはズレるよねといった話をするためにMiroで図を書いたりします。レトロスペクティブの際などにこれを眺めて色々組み替えたりします。
例: リリースフローを図にする
病院向けのプロダクトは、バグの混入やデグレなどをできるだけ防ぐために厚めのリリースフローになっています。リリース日が決まっており前日から準備するスタイルで、リリース担当を持ち回りで行います。リリースフローのドキュメントはありますが、リリース内容によって手順が異なる場合があったり、不具合が見つかった場合のフローなど複雑なので、図にもしつつ、認識わせしたり自動化可能なポイントを洗い出したりしています。
例: ユーザーストーリーマッピングを行う
チーム結成当初やクォータの区切り目などで、短期中期でフォーカスするものなどについて認識合わせしたり議論するために、ユーザーストーリーマッピングを行ったりしてます。スライスをざっくり置いてますがそこまで厳密に運用はせず、目線合わせを主な目的として使っています。
例: 座席表を作る
座席表なんかも作ったりします。そろそろ40名を超えてきてかなり手狭になってきました...
ここがいいよMiro
ということで、Miroは概ね何にでも使えて便利です。特に強力だなと実感するのは、リモートで集まって議論する場合です。
リモートでもホワイトボードで議論してる感覚に近づける
さすがに物理ホワイトボードの体験にはかなわないんですが、リモートメンバーがいるミーティングをする場合などはMiroのほうが捗ります。
エンジニアが集まってミーティングする様子(わかりづらいけどリモートメンバーもいます)。
Ubieはその日にリモートするかは各自で決めるほか、フルリモートのメンバーもいたりします。ミーティングする際は大画面にGoogle Hangoutsを映して、画面共有しながら会話します。 設計議論のときなんかはリモートメンバーもMiroをいじりつつ会話しつつで概ね対面に近い成果物が得られます。また電子化された状態で残るのもいいですね。
ここが気になるMiro
複数人で図を書くという体験については申し分のないMiroですが、一点使っててしんどい部分があります。
- 図の一覧画面で図のサムネイルが出ない(なんか設定する必要あるのかな?)
- デフォルトでソート条件が last opened になっていて他者が作った新規の図が一生見つからない
後者はまぁソート条件変えればいいんですが、デフォルトはlast modifiedとかにしてほしいですね...、前者についてもまぁ空間が無制限なのでどこを切り取るか難しいってのはありつつ基本ノープレビューは厳しい感じがあります。こうするといいよみたいな方法あったらぜひ教えてください。
おわりに
ざっくりですがMiroを紹介しました。Ubieに入社したときはすでにMiroが導入されていたので、Ubieでの業務的な前後の比較は僕はできないですが、前職含めてわりとホワイトボードと付箋最強でやっていたので初めて触ったときは結構衝撃を受けました。ぜひ一度試してみてください!
Ubieってどんな会社なんと思ったかたはWe-are-Ubie-会社・事業・組織・採用のことを是非御覧ください(やや画像重いかもです...)。
ノンアルコールビールめっちゃ増えてる
昨年のノンアルコールビールで晩酌すると風呂上がりにさっぱりプログラミングできて助かる - visible trueに引き続き、今年も書きます。まるで酔っ払っているかのように雑です。
自我を失ったノンアルコールビールたち
さて個人的にノンアルコールビールではアサヒドライゼロが好きで、いつもノンアルコール飲料を選ぶ時はドライゼロを買っています。
ある日、アサヒドライゼロを仕入れるか〜とスーパーに赴いたら新たなノンアルコールビールたちが棚にひしめき合っていました。
www.asahibeer.co.jp www.kirin.co.jp www.suntory.co.jp www.sapporobeer.jp
おいおいおいおいおい 「糖の吸収を抑える」、「脂肪を減らす!」 アルコールもプリン体も失い、更に糖の吸収を抑え脂肪を減らす機能を付与されるとは。 バーソロミュー・くまか。
飲んでみる
6缶入りはパッケージにでかでか「脂肪を減らす!」って書いてあってちょっと恥ずかしかったので1缶ずつ買いました。
味はどれもおいしく、値段も手頃*1だし良い感じでした。この中ではカラダフリーが香りが好みで一番スキな感じでした。パッケージさえ気にならなければドライゼロと交互に飲もうと思うくらい。
ノンアルコール充実してきた
ビール系だけでも結構増えてるけど、日本酒系なんかも出てきてノンアルコールアツいですね。
酒税かからないから安いし、アルコール入ってないので健康的、美味しい。うれしい。
*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でできることは次の通りです。
- 指定したパス内のマイグレーションファイル(.sql)を収集し実行する。
- 実行したマイグレーションファイルのバージョン情報を永続化し、バージョンに差分があればマイグレーションを実行する
- 実行済みのマイグレーションと、収集したマイグレーションファイルのchecksumを比較して、変化を検出した場合はエラーを送出する
使い心地としては概ね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を作る
次にSpannerKaseDatabaseClient
とMigrationDataScanner
を作ります。
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は割とレアな気がしますが、もし機会があれば触ってみてください。
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ど導入する方法について書いてある。
Jetpack Compose チュートリアル
Jetpack Composeの基本の解説。Composable functionsやレイアウト、スタイル、Themeなどの説明。 developer.android.com
Codelab
↑のドキュメントがCodelabになった感じ。わかりやすい。 codelabs.developers.google.com
Sample project
JetNewsというアプリのサンプル。 ナビゲーションドロワー、画面遷移などJetpack Composeで全て行っている。 データは埋め込みなので通信処理等はない。かなり参考になる。
Sample project2
自分で作ったもの。Navigation、ViewModelを使う。 Github APIをRetrofitで実際に検索する。 既存プロジェクトとの共存をイメージしているけど、JetNewsがSPAぽい世界観なので迷っていきている。
おわりに
まだまだ進化中という感じだけどいい感じなのではという気持ち。