スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Jetpack Compose を使って実際に画面を作ってみて

こんにちは。Android アプリ開発者の geckour です。
前回はデザインシステムの実装についての話をしましたが、今回はそちらを実際に運用してみた中間報告をしたいと思います。

はじめに

前回は、Jetpack Compose によってデザインシステムを構築するところまでを取り上げました。
そこで、実際に構築したデザインシステムを使って画面を組み上げてみた結果、どのような事が起こったかを紹介していきます。

こつこつやろう

まずは頑張りすぎて失敗した話からです。

新規 or 小さな画面から

最初はログイン画面など小さな画面からやっていたのですが、ある程度慣れてきた頃 調子に乗って 最も複雑かつ大きな既存の画面のリプレイスにトライしました (ここで色々な知見を得て後の導入をスムーズにする狙いがあった) 。

狙い通り知見はたくさん得られたのですが、いくつか問題がありました。

  • 改修に時間がかかりすぎて該当画面に新規機能が次々に追加され、その実装は一旦旧画面実装上にされるので新画面実装上に毎回移植せねばならず、二度手間かつバグを生みやすい
  • 小さな画面で揉んでいけば段階的に進んだはずのデザインシステム実装の修正が、大きな画面でいきなりやることによって差分が巨大になってしまう

などなど…

もし可能であるならば、焦らず小さな画面や新しい画面からこつこつと試していくことを強くお勧めします。

Jetpack Compose で色々やってみた結果

次に、色々とチャレンジしていく中で分かってきたことをまとめてみたいと思います。

アニメーションと Jetpack Compose

Jetpack Compose もアニメーション API が結構充実していて、Android View で実現できるものは代替できると言っていいと思います。
弊プロダクトでは、AnimatedVisibilityanimate*AsState などを利用して、触って気持ちの良い UI を実現しています。

Android View に対してのアニメーションの実装だと、状態の管理とビューの定義が離れたところにある点などで見通しが悪く扱いづらい場面もあったのですが、Jetpack Compose ではそれらのやりにくさが解消されました。

実際に伸縮・展張可能な Card を作った際のサンプルコードを置いておきます。

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ExpandableCard(
    modifier: Modifier = Modifier,
    name: String,
    childNames: List<String>,
    isExpanded: Boolean,
    onClick: () -> Unit
) {
    Column {
        Card(
            modifier = modifier,
            shape = if (isExpanded) {
                RoundedCornerShape(
                    topStart = CornerSize(6.dp),
                    topEnd = CornerSize(6.dp),
                    bottomStart = CornerSize(0.dp),
                    bottomEnd = CornerSize(0.dp)
                )
            } else RoundedCornerShape(6.dp),
            backgroundColor = Color.White,
            elevation = 4.dp,
            onClick = onClick
        ) {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            ) {
                Text(
                    text = name,
                    modifier = Modifier.fillMaxWidth(),
                    style = TextStyle(fontSize = 14.sp)
                )
            }
        }
        childNames.forEachIndexed { index, childName ->
            AnimatedVisibility(visible = isExpanded, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically()) {
                Child(
                    modifier = Modifier.fillMaxWidth(),
                    name = childName,
                    isBottom = childNames.lastIndex == index
                )
            }
        }
    }
}

@Composable
fun Child(
    modifier: Modifier,
    name: String,
    isBottom: Boolean
) {
    Card(
        modifier = modifier,
        shape = if (isBottom) RoundedCornerShape(
            topStart = CornerSize(0.dp),
            topEnd = CornerSize(0.dp),
            bottomStart = CornerSize(6.dp),
            bottomEnd = CornerSize(6.dp)
        ) else RectangleShape,
        elevation = 4.dp,
    ) {
        Divider()
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = name,
                style = TextStyle(fontSize = 14.sp)
            )
        }
    }
}

@Preview
@Composable
fun ExpandableCardPreview() {
    var expanded by remember { mutableStateOf(false) }
    Box(modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)) {
        Card.ExpandableCard(
            name = "タイトル",
            childNames = listOf("子タイトル 1", "子タイトル 2", "子タイトル 3"),
            isExpanded = expanded,
            onClick = {
                expanded = !expanded
            }
        )
    }
}

グラフと Jetpack Compose

Jetpack Compose を使っていて感動したことの一つに、グラフの作りやすさがあります。
データを操作するコードとビューを生成するコードが近いところに置け、また階層構造が際立つので、見通しが良くなるからではないかと思います。

シンプルなものであればライブラリを用いずともかなりサクッと作れてしまうので、試してみてはいかがでしょうか。

@Composable
fun Bar(value: Int, max: Int) {
    val heightFraction = remember { Animatable(0f) }
    LaunchedEffect(Unit) {
        if (max > 0) {
            heightFraction.animateTo(value.toFloat() / max, animationSpec = tween(durationMillis = 1000))
        }
    }
    Box(modifier = Modifier.fillMaxHeight()) {
        Box(
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .background(
                    color = Color.Blue,
                    shape = RoundedCornerShape(4.dp, 4.dp, 0.dp, 0.dp)
                )
                .width(16.dp)
                .fillMaxHeight(heightFraction.value)
        )
    }
}

@Composable
fun Graph(weeklyUsage: List<Int>) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.SpaceBetween
        ) {
            repeat(3) {
                Divider()
            }
        }
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
            val max = weeklyUsage.maxOf { it }
            weeklyUsage.forEach { dailyUsage ->
                Bar(dailyUsage, max)
            }
        }
    }
}

Epoxy と Jetpack Compose

Epoxy の 1 アイテムとして Jetpack Compose を利用したい場合も、Android View との相互運用性を利用すれば可能です。

ただし、epoxy-compose の依存を追加した上で composableInterop() を利用して item を定義する必要がある (item()ComposeView を利用して定義するとうまく動きませんでした) ので注意が必要です。

OK

override fun buildModels(data: HogeData?) {
    composableInterop("hogeItem", key) {
        SomeCoolComponent()
    }
}

NG

override fun buildModels(data: HogeData?) {
    item {
        composeView {
            SomeCoolComponent()
        }
    }
}

Visual Regression Testing と Jetpack Compose

Jetpack Compose とライブラリを組み合わせることで、Visual Regression Test (以下 VRT、VRT とは?という方はこちら) が格段に簡単に実行できるようになりました。

こちらの発表スライドでも少し触れているのですが、Showkase というライブラリを使って Jetpack Compose のプレビューに少し手を加えることで、簡単に VRT を実行することができます。

おわりに

Jetpack Compose を使った画面構成に段階的に移行していくのは、小さなつまづきはありつつもそこまで難しくないこと、また Jetpack Compose を利用しているからこその恩恵について簡単に紹介しました。

皆さんのプロダクトでは Jetpack Compose は使われていますか?
もしまだでしたら少しずつ試してみてもいいかもしれません!

それでは!


私達のチームでは、一緒に最高のプロダクトを作っていってくれる仲間を募集しています!
少しでもご興味がある方は こちら のページからご連絡ください!