Composeで敷き詰めるUIを どうやって作るか

みなさんこんにちは、プロダクト開発部でAndroidアプリを開発しているカーキです。

先週の10月25日に開催されたmobile.stmnにて「Composeで敷き詰めるUIをどうやって作るか」というタイトルで発表をしました。登壇した資料自体はSpeakerDeckにも公開をしていますが、せっかくならと思いテックブログでも公開してみることにしました。

導入

タイトルに「敷き詰めるUI」と書いていますが、これは以下の画像のように水平・鉛直方向へ要素が敷き詰められるようなレイアウトを指しています。

敷き詰めるレイアウトの例

このようなレイアウトをJetpack Composeで実現する方法として、2つのレイアウト方法が提供されています。

  1. Lazy〇〇Grid
  2. Flow〇〇

Lazy〇〇Gridとは、LazyVerticalGridやLazyHorizontalGridなどに代表されるレイアウトコンポーネントです。またFlow〇〇とは、FlowRowやFlowColumnなどに代表されるレイアウトコンポーネントです。それぞれのレイアウトコンポーネントに関して、以下で基本的な性質から紹介していきます。

Lazy〇〇Gridについて

基本性質

名前の通り Lazy〇〇Grid は格子状のレイアウトを遅延表示することができるレイアウトコンポーネントになります。LazyColumnやLazyRowと同じようにcontentブロック内では、itemitemsなどのメソッドが利用できます。

主にLazyVerticalGridとLazyHorizontalGridという2つのコンポーネントに分けられ、それぞれ以下のように要素が敷き詰められます。

Lazy〇〇Gridの並び方

LazyVerticalGridは垂直方向へ敷き詰めが行われ鉛直方向にスクロール可能になり、LazyHorizontalGridは鉛直方向へ敷き詰めが行われて垂直方向へのスクロールが行われるため、注意が必要です。

LazyStaggeredGridについて

LazyVerticalGrid, LazyHorizontalGridでは、格子状のレイアウトになるため、要素の大きさは均一になります。ただこれらのレイアウトと近縁関係にあるLazyStaggeredGridを利用することで高さもしくは、横幅の大きさを可変なものとしてレイアウト可能になります。

公式ドキュメントより LazyStaggeredGrid の実装例

LazyStaggeredGrid は執筆時点(2024/11/01)で Experimental なレイアウトとなっています。

GridCells

LazyHorizontalGridでの格子の敷き詰め方はcolumsというパラメータによって決まります。(LazyHorizontalGridの場合はrowsというパラメータ)

columnsGridCellsを継承したクラスを要求しており、compose.foundationでは以下の3つのクラスが用意されています。

  1. GridCells.Fixed
  2. GridCells.Adaptive
  3. GridCells.FixedSize

1. GridCells.Fixed

Fixedでは、一列の中に配置する要素の個数を指定することができます。

GridCells.Fixedで指定した場合

指定された個数に従って要素の大きさが決まります。

2. GridCells.Adaptive

Adaptiveでは、一列中の要素の幅の最小値を指定することができます。(LazyHorizontalGridの場合は高さの指定になります)

GridCells.Adaptiveで指定した場合

指定された幅の最小値に従って表示することの可能な個数分配置されます。指定しているのはあくまで最小値になるため、親のレイアウトの幅が88.dpで、Adaptive(20.dp)と指定した場合は、一列中に4つの要素が配置され、その幅は22.dpとなります

3.GridCells.FixedSize

FixedSizeでも、一列中の要素の幅(もしくは高さ)を指定することで配置を指定します。ただAdaptiveと異なっているのは、列中で余った領域に関してはArrangement.Horizontal の指定に従うようになっています。例えば以下の画像では、Arrangement.Horizontalの指定をArrangement.spaceBy(8.dp)としているため、要素間の間隔を8.dpにするために余剰分に関しては、画面右に寄せられています。

GridCells.FixedSizeで指定した場合

ヘッダーの表示

LazyGridのitemとして、ヘッダー要素を表示させることができます。ただ普通にitemのブロックに要素を入れただけでは、下画像のようにグリッドの要素として組み込まれてしまいます。

item のブロックに単純にヘッダー要素を入れた場合

以下のようにspanを実装する必要があります。

@Composable
fun LazyGridHeaderSample() {
   LazyVerticalGrid(
      columns = GridCells.Fixed(3)
   ) {
      
      item(
         span = { GridItemSpan(maxCurrentLineSpan) }
      ) {
         Text(text = "Header")
      }
      
      items(10) { index ->
         ...
      }
   }
}

spanmaxCurrentLineSpan で指定すれば、空いているGridのスペース分の領域をコンテンツブロック内で確保することができます。上記のコードで並べた結果が以下のようになります。

GridItemSpanを利用してitem のブロックに単純にヘッダー要素を入れた場合

GridItemSpanを利用することでLazyVerticalGridを利用してヘッダーを表示することができます。

Flowレイアウト

基本性質

Flowレイアウトは公式のドキュメントでは、「ColumnやRowのコンテナのスペースが不足した時に、要素が次の行に流れ込む性質を持ったコンポーネント」と説明されています。次の行に流れ込む性質を持ったColumnやRowと説明されている通り、レイアウト方向に応じてFlowColumnやFlowRowなどが存在します。Flowレイアウトは執筆時点(2024/11/01)で、ExperimentalLayoutApi とされており、試験運用版という扱いになっています。

Flowを使うことで以下のようなタグの表示を実現することができます。

公式ドキュメントよりFlowレイアウトの例

Flowレイアウトでは、上記のタグのように要素を並べていく方向の幅を可変にすることができます。LazyVerticalGridでは基本的に並べる方向の幅は全て均一になります。格子状に並べる上ではLazyGridが適していますが、サイズの異なる要素を順に並べていくのはFlowレイアウトが適しています。

Arrangement

Flowレイアウトの敷き詰め方は、HorizontalArrangementverticalArrangementという引数に何を渡すかによって決まります。これらのパラメータへの指定によって右寄せ・左寄せ・等間隔など様々な配置方法を指定することができます。

この辺りは公式のドキュメントが詳しいので説明はそちらに譲ります。

2つの使い分け

ここまでLazyGridとFlowレイアウトの性質について紹介してきました。ざっくりそれぞれのレイアウトコンポーネントの特徴をまとめると以下のようになります。

  • LazyGridは、格子状に要素を敷き詰めるのに適したレイアウトコンポーネント
  • Flowレイアウトは、折り返しを考慮してものを敷き詰めたい時に適したコンポーネント

この2つの比べると格子状の場合はLazyGridで、それ以外の場合はFlowを選択するのが良いように感じます。ただLazyVerticalGridはLazyなレイアウトになるので、同一方向のLazyのレイアウトを親や子に持てないなどといった、それぞれ採用判断に影響する差異が存在するので紹介します。

LazyGridが向いているケース

  • スクロール方向と交差する方向のサイズが固定である
  • 表示量が多いなど、遅延して表示したい要件がある
  • Experimental な機能を利用できない

Flowレイアウトが向いているケース

  • 要素の大きさがものによって変わる
  • 遅延して表示させる要件がない
  • グリッドの部分のみ背景を変えたい要件がある

「Flowレイアウトが向いているケース」の最後の項目だけより具体的な事例を出していますが、TUNAGの開発中にこのパターンに当たり、Flowレイアウトを採用したという背景があります。

具体的には以下のような画面になります。(該当の画面を抽象化して示しています)

特殊な実装例

こちらの画面では均等に要素を並べるようなUIを想定していたため、LazyVerticalGridの採用を検討していましたが、グリッドの部分のみ背景色を変更することがLazyGridではできません。この画面はスクロールも要求していたので、LazyVerticalGridで実装する場合画面全体をLazyVerticalGridでレイアウトする必要がありました。ただ特定のitemsのブロックの中でのみパディングや背景色の変更を行うことが難しかったため、FlowRowを採用しました。

何か複雑に組み合わせることで、LazyVerticalGridの利用も可能だったかもしれませんが、今後のメンテナンス性も考えると、シンプルな利用で表現することができるFlowRowに寄せることとしました。

LazyGridとFlowレイアウトでは、それぞれ向いているレイアウト方法がありますが、どちらのレイアウト方法でも実現することができるのもまた事実です。基本的にはそれぞれのレイアウトコンポーネントの基本性質にあった方法を取れるのが良いですが、それでも実現することが難しい場合・実装があまりにも複雑になりすぎてしまう場合があります。それぞれのメリット・デメリットのトレードオフを考えて実装されるのが良いかと思います。

まとめ

今回のブログでは、LazyGridとFlowレイアウトの紹介と比較を行いました。

どちらでもできること・どちらかにしかできない表現があるため、それぞれの性質を理解した上で、どちらを利用するのかを要件に沿って選ぶのが良いと思います。

株式会社スタメンでは、チームで議論を深めながらJetpack Composeの利用の幅を広げています。Jetpack Composeが大好きなAndroidエンジニアの方はぜひ一度カジュアル面談からお話を聞いてみてはいかがでしょうか。

herp.careers

herp.careers