2021/06/15

Jetpack Composeで一覧表示に利用するCard Viewを実装してみる

jetpack-composekotlin

以下のような構成のCard ViewをJetpack Composeで作成しました。

card-view-min.png

  • バナー画像
    • カード全体にfitするように表示する
    • UIのメインスレッドとは非同期で読み込みを行うようにする ← Coilの出番
  • タイトル
  • タグ(Chips)
    • 複数あったときに複数行に渡って折り返して(wrapped)表示されるようにする ← FlowLayoutの出番
  • サブタイトル

準備

以下のライブラリをインストールしておきます。

dependencies {
    final def accompanist_version = '0.11.0'
    implementation "com.google.accompanist:accompanist-coil:$accompanist_version"
    implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
}

Cardに表示するModelの定義

@Immutable
data class Activity(
    val id: Long,
    val slug: String,
    val title: String,
    val subtitle: String?,
    val bannerImageUrl: String?,
    val tags: Array<String>,
)

Card Viewの実装

ポイント

  • rememberCoilPainterを使って非同期でインターネットから画像をダウンロードして表示されるようにする
  • FlowRowを使って、タグが複数あり、1行で表示されない場合は折り返されるようにする
@Composable
fun ActivityCard(
    activity: Activity,
    onClick: () -> Unit,
) {
    val elevation = 8.dp
    Card(
        shape = MaterialTheme.shapes.medium,
        modifier = Modifier.fillMaxWidth(),
        backgroundColor = MyTheme.colors.getUiBackgroundForElevation(elevation = elevation),
        elevation = elevation
    ) {
        Column(
            modifier = Modifier
                .clickable(onClick = onClick)
                .fillMaxWidth()
        ) {
            activity.bannerImageUrl?.let { bannerImageUrl ->
                Image(
                    painter = rememberCoilPainter(bannerImageUrl),
                    contentDescription = null,
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(225.dp),
                    contentScale = ContentScale.Crop
                )
            }
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = activity.title,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                style = MaterialTheme.typography.subtitle2,
                color = MyTheme.colors.content,
                modifier = Modifier.padding(horizontal = 16.dp)
            )
            if (activity.tags.isNotEmpty()) {
                Spacer(modifier = Modifier.height(4.dp))
                FlowRow(
                    mainAxisSpacing = 4.dp,
                    crossAxisSpacing = 4.dp,
                    modifier = Modifier.padding(horizontal = 16.dp),
                ) {
                    activity.tags.forEach { tag ->
                        Chip(text = tag)
                    }
                }
            }
            activity.subtitle?.let { subtitle ->
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = subtitle,
                    style = MaterialTheme.typography.caption,
                    color = MyTheme.colors.contentSecondary,
                    modifier = Modifier.padding(horizontal = 16.dp)
                )
            }
            Spacer(modifier = Modifier.height(16.dp))
        }
    }
}

プレビューで確認

@Preview("activity card default")
@Preview("activity card dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ActivityCardPreview() {
    MyTheme {
        val activity = Activity(
            id = 1L,
            slug = "copy-text-to-clipboard-with-vuejs",
            title = "Vue.jsでクリップボードにコピーするコントロールを作る",
            subtitle = "Vue.jsのボタンクリックでテキストをクリップボードにコピーしてみました",
            bannerImageUrl = "https://api.kumano-te.com/api/v1/activities/32/banner?rev=100",
            tags = arrayOf("vuejs", "nuxtjs", "javascript", "clipboard", "hoge", "fuga", "hogehoge", "fugafuga"),
        )
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
            modifier = Modifier.fillMaxWidth().padding(8.dp)
        ) {
            ActivityCard(
                activity = activity,
                onClick = {}
            )
        }
    }
}

以上です。