visible true

技術的なメモを書く

Jetpack Compose 0.1.0-dev05で追加されたAdapterListを眺める

Jetpack Compose 0.1.0-dev05がリリースされましたね。0.1.0-dev05でui-foundationAdapterListというComposableが追加されました。

待望のAdapterList

AdapterListの説明は次のようになっています。

A vertically scrolling list that only composes and lays out the currently visible items.

今まではRecyclerViewのようなComposableが存在せず、VerticalScrollerを使ってそれっぽい動作をしていましたが、VerticalScrollerはScrollViewと同じものなので実用には限界がありました。

AdapterListはRecyclerViewと同じように、表示されている要素だけをレンダリングするので、大量の要素があってもサクサク動作します。

AdapterListの使い方

AdapterListのシグネチャは次の通りです。

@Composable
fun <T> AdapterList(
  data: List<T>,
  modifier: Modifier = Modifier.None,
  itemCallback: @Composable() (T) -> Unit
)

実際に使うには次のようになります。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.ui.core.Text
import androidx.ui.core.setContent
import androidx.ui.foundation.AdapterList
import androidx.ui.layout.Container
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.LayoutWidth
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Card
import androidx.ui.unit.dp

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MaterialTheme {
        val data = 0.until(100).toList()
        AdapterList(data = data) {
          Card(modifier = LayoutPadding(top = 8.dp, bottom = 8.dp, left = 16.dp, right = 16.dp)) {
            Container(modifier = LayoutWidth.Fill + LayoutPadding(16.dp)) {
              Text("Hello ${it}")
            }
          }
        }
      }
    }
  }
}

f:id:sys1yagi:20200226163112p:plain:w250

AdapterListはまだ実用できない

0.1.0-dev05の時点ではまだ2コミットしかないので、実用に足らないのは当然っちゃ当然かなと思います。 https://android.googlesource.com/platform/frameworks/support/+log/refs/heads/androidx-compose-release/ui/ui-foundation/src/main/java/androidx/ui/foundation/AdapterList.kt

具体的な問題としては 要素のクリックイベントが動作しない という点があります。例えば次のコードのButtonはうまく動作しません。

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MaterialTheme {
        val data = 0.until(100).toList()
        AdapterList(data = data) { value ->
          Card(modifier = LayoutPadding(top = 8.dp, bottom = 8.dp, left = 16.dp, right = 16.dp)) {
            Container(modifier = LayoutWidth.Fill + LayoutPadding(16.dp)) {
              Button(onClick = {
                println("click! $value")
              }) {
                Text("button ${value}")
              }
            }
          }
        }
      }
    }
  }
}

いくらか実験してみたところ、Ripple要素があるとクリックイベントが実行されないようです。Buttonは Ripple + Clickableで構成されているので反応しなくなっているようです。Rippleを使わずClickableのみを使えば動作はするのですが、タッチフィードバックがなくなるので厳しいです。

もちろんこうした問題は今後どんどん改善されていくと思いますが、今すぐに使うというのはちょっと難しそうです。

おわりに

ついにAdapterListが登場して実用段階への光が射してきましたね。さすがにプロダクション投入はまだまだ難しいですが、一部のViewをComposeに置き換えるのは十分できそうだなぁと思います。

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

Jetpack Compose 0.1.0-dev05が出ました。 リリースノートはこちら https://developer.android.com/jetpack/androidx/releases/compose#0.1.0-dev05

前回に引き続き、アップデートでエラーになる部分と対応方法を紹介していきます。

androidx.ui.layout.Paddingが廃止

androidx.ui.layout.Paddingが廃止され、androidx.ui.layout.LayoutPaddingになりました。PaddingはComposableだったのに対して、LayoutPaddingはModifierです。次のような使い方になります。

before

Padding( 8.dp ) {
    Text(text = "こんにちは")
}

after

Text(
    modifier = LayoutPadding( 8.dp ),
    text = "こんにちは"
)

FontFamilyのコンストラクタがprivateになった

カスタムフォントを使う時はres/font にフォントファイルを置きつつ、FontFamilyを作って利用する形だったのですが、FontFamilyのコンストラクタがprivateになり、作り方が変わりました。

before

FontFamily(
  // res/font/ipam.ttf にファイルを置いておく
  Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal)
)

after

font(R.font.ipam, weight = FontWeight.W400, style = FontStyle.Normal)

Ambient.ofが廃止

Ambient.ofの代わりにambientOf関数が生えました。Providerも独立して、Providersになりました。Provideする値増えたらネスト大変だなーと思ってたところなのでちょうどいいです。

before

val IpamFontAmbient = Ambient.of<FontFamily>()

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

after

val IpamFontAmbient = ambientOf<Font>()

@Composable
fun IpamFontProvider(children : @Composable() () -> Unit) {
    val font = remember {
        font(R.font.ipam, weight = FontWeight.W400, style = FontStyle.Normal)
    }
    Providers(IpamFontAmbient.provides(value = font), children = children)
}

ambient関数が非推奨

上位のComposableからProvideされる値を取り出す時はambient関数を使ってましたが、非推奨になりました。代わりにambient関数にkeyとして渡しているAmbilentインスタンスcurrentを使います。

before

val font = ambient(IpamFontAmbient)

after

val font = IpamFontAmbient.current

androidx.ui.core.ambientDensity関数が廃止

Composable内でdensityを取り出せるandroidx.ui.core.ambientDensity関数がなくなりました。

before

import androidx.ui.core.ambientDensity

val density = ambientDensity()

after

import androidx.ui.core.DensityAmbient

val density = DensityAmbient.current

withDensity関数が廃止

withDensity関数は、DensityScope.() -> Unitを受け取ることで、Dp.toPx関数などの拡張関数を使えるスコープを提供するのですが、なくなりました。代わりにDensityScopeと同様の拡張関数を持つDensity interface が用意されました。DensityAmbient.currentでDensityインスタンスを取り出せるので、kotlinのwith関数を使って同じことができます。

before

withDensity(density) {
    Paint().apply {
        isAntiAlias = true
        style = PaintingStyle.stroke
        this.strokeWidth = strokeWidth.toPx().value
    }
}

after

val density = DensityAmbient.current

with(density) {
    Paint().apply {
        isAntiAlias = true
        style = PaintingStyle.stroke
        this.strokeWidth = strokeWidth.toPx().value
    }
}

ImageがPainterに変更

画像の表示はImageを使ってましたがPainterという抽象クラスが使われるようになりました。VectorAssetはImageになれないので、Painterで抽象化するのかなと思ったらVectorPainterはまだないみたいです。

before

AppBarIcon(
  icon = imageResource(R.drawable.ic_baseline_arrow_back_24),
  onClick = {
    backStack?.pop()
  }
)

after

AppBarIcon(
  icon = ImagePainter(imageResource(R.drawable.ic_baseline_arrow_back_24)),
  onClick = {
    backStack?.pop()
  }
)

Buttonの引数が変更

styleが展開されたほか、textがなくなりchildrenになりました。

before

Button(
    "キャンセル",
    style = ButtonStyle(
        backgroundColor = MaterialTheme.colors().secondary,
        contentColor = MaterialTheme.colors().onSecondary,
        shape = MaterialTheme.shapes().button,
        elevation = 2.dp
    ),
    onClick = onCloseRequest
)

after

Button(
    backgroundColor = MaterialTheme.colors().secondary,
    contentColor = MaterialTheme.colors().onSecondary,
    shape = MaterialTheme.shapes().button,
    elevation = 2.dp,
    onClick = onCloseRequest
) {
    Text("キャンセル")
}

終わりに

結構大変だった...。でもだいぶ洗練されてきた印象がある。今後が楽しみです。

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円/缶くらい