Jetpack Composeで表形式のビューを作る

Jetpack Composeで、マークダウン等で書くような表形式のViewを作る際にどうしたかをメモします。

## 表形式ビューのイメージ
gitのwikiなんかにたまに書くやつです。以下のようなイメージです。

|  果物  |  色      |  味      |
|-------|---------|---------|
| リンゴ | 赤      | 甘い    |
| バナナ | 黄色    | 甘い    |
| グレープ | 紫     | 甘い    |

とある案件のPoCフェーズでこういった形式のビューを表示して欲しいと依頼があったため、考えることになりました。
正直、リストやグリッドでどうにでも表現出来そうなのですが(おそらくそちらの方が楽)、デザインも特に決まっていなかったのと、未だ他案件で採用できていないJetpack Composeを学ぶ良い機会だと思ったため、そのまま実装してみることにしました。

## 下調べ

https://developer.android.com/jetpack/compose/lists?hl=ja

公式の「リストとグリッド」ページです。ColumnやLazyRow、LazyVerticalGridなど、Jetpack Composeユーザーにはお馴染みの概念がずらりと紹介されていますが、今回表現したいビューの実装にもやはりこれらを応用していく必要があるだろうと予想しました。

据え置きで`Table`みたいなUI functionがないかと探してみましたが、見つかりませんでした。(個人的にアプリとしてそこまで一般的な表現ではない気もします)

## 実装

改めて、作りたいイメージはこんな感じです。Table全体およびヘッダーはスクロールせず、中身だけをスクロールさせます。

![table.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472463/f18fd4b3-46f8-207f-195f-cf5f4b4dc5d0.png)

#### ViewData

表に示す行をモデリングします。composablesは、行の中に表示する複数のUI要素を示しています。また、onClickはその行がタップされたときの関数型のプロパティです。

```TableRowViewData.kt
interface TableRowViewData {
    val weights: List<Float>
    /** 行単位でのComposable群 */
    val composables: List<@Composable () -> Unit>
    val onClick: (() -> Unit)?
}

/** ヘッダー */
data class TableHeaderRowViewData(
    override val weights: List<Float>,
    override val composables: List<@Composable () -> Unit>,
    override val onClick: (() -> Unit)? = null
) : TableRowViewData
```

#### View (UI Function)

ViewDataを渡して表示するComposable関数です。表形式における線を`Divider`を使って組み合わせることがポイントだと考えています。

```Table.kt
/** 表形式で表現するビュー */
@Composable
fun <T : TableRowViewData> Table(
    columnCount: Int,
    rowDataList: List<T>
) {
    Column {
        // ヘッダー(スクロールしない固定部分)
        Divider(color = colorResource(id = R.color.greyCCCCCC))
        TableRow(viewData = headerRowData, columnCount = columnCount)
        Divider(color = colorResource(id = R.color.greyCCCCCC))

        // スクロール部分
        LazyColumn {
            // 行を並べていく
            items(rowDataList) {
                TableRow(viewData = it, columnCount = columnCount)
                // 各行の下部の区切り線(最下部も含む)
                Divider(color = colorResource(id = R.color.greyCCCCCC))
            }
        }
    }
}

@Composable
private fun TableRow(viewData: TableRowViewData, columnCount: Int) {
    Row(
        Modifier
            .height(IntrinsicSize.Min)
            .clickable(enabled = viewData.onClick != null) {
                viewData.onClick?.invoke()
            }
    ) {
        // 行の左端の区切り線
        TableRowDivider()
        (0 until columnCount).forEach { index ->
            Surface(modifier = Modifier.weight(viewData.weights[index])) {
                viewData.composables[index]()
            }
            // 各フィールドの右端の区切り線
            TableRowDivider()
        }
    }
}

/** 縦線 */
@Composable
private fun TableRowDivider() {
    Divider(
        Modifier
            .fillMaxHeight()
            .width(dimensionResource(id = R.dimen.table_row_divider_stroke))
    )
}
```

Lazyレイアウトにおける`item`に複数の要素を含めることはパフォーマンス上よろしくありませんが、区切り線に関してはそちらの影響は無く、あるいはスクロールのインデックスが変わることもありません。

また、仮に親のColumnがスクロール可能な場合、サイズの決まっていない同方向にスクロール可能なLazyColumnが子にあるためIllegalStateExceptionを投げられて落ちますが、今回はColumnにそのような設定を施していないため問題ありませんでした。Table自体をスクロールさせたい場合は、LazyColumnを親としてラップするか、子であるLazyColumnのサイズをモディファイアで先に設定しておくなどする必要がありそうです。

***
以上、表形式というカスタムビューを実装してみた件でした。色々と表現の幅がありそうで楽しそうですが、やたらとガチャガチャ組み合わせると訳のわからないComposable関数が出来上がるかもしれません。。