Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。
そのライブラリ中のLazyColumnについて、構成や使用方法などをまとめます。
※環境:Android Studio Koala | 2024.1.1
    Kotlin 1.9.0
    Compose Compiler 1.5.1
    androidx.compose.foundation:foundation 1.6.8
目次
UIの概要
LazyColumnは「複数のデータやオブジェクト」または「コレクション」を一覧(リスト)表示するUIです。
1つのデータや要素をアイテムとして、縦(列:Column)方向に並べます。

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        @OptIn(ExperimentalFoundationApi::class)
        stickyHeader {  // ← 実験的な実装
            headerItem()
        }
        items(playerList) { item ->
            playerItem(item.name, item.level, item.score)
        }
    }

関数の引数
LayzyColumnの状態(装飾、演出など)は、引数により指定できます。
@Composable
fun LazyColumn(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    verticalArrangement: Arrangement.Vertical =
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    userScrollEnabled: Boolean = true,
    content: LazyListScope.() -> Unit
) { ... }
| 引数 | 概要 | |
|---|---|---|
| modifier | Modifier | UI全般のCompose修飾子 | 
| state | LazyListState | スクロールの状態を監視・制御 | 
| contentPadding | PaddingValue | コンテンツ(リスト)の周りの空白 | 
| reverseLayout | Boolean | アイテムの並びを反転 false:正転(上から下へ並べる)、デフォルト true:反転(下から上へ並べる) | 
| verticalArrangement | Arrangement.Vertical | コンテンツ(リスト)の配置、垂直方向 ※詳細は「配置パラメータ」を参照 ※詳細は「リストの配置」を参照 | 
| horizontalAlignment | Alignment.Horizontal | コンテンツ(リスト)の配置、水平方向 ※詳細は「配置パラメータ」を参照 ※詳細は「リストの配置」を参照 | 
| flingBehavior | FlingBehavior | スクロールの動きを定義 | 
| userScrollEnabled | Boolean | ジェスチャーによるスクロールの許可 false:不許可 true:許可、デフォルト ※ジェスチャー:画面のスワイプ、フリック | 
| content | LazyListScope.() -> Unit | コンテンツ(リスト)のスロット ・LazyListScope DSLが利用可能 ・DSLコマンドでアイテムを並べて配置 | 
| 配置パラメータ | 概要 | |
|---|---|---|
| Alignment | CenterHorizontally | 水平方法の中央 | 
| Start | 左端(デフォルト) | |
| End | 右端 | |
| Arrangement | Center | 垂直方向の中央 | 
| Top | 上端(デフォルト) | |
| Bottom | 下端 | |
| SpaceAround | 空きスペースの均一化 ※詳細は「特殊な配置1」を参照 | |
| SpaceBetween | ||
| SpaceEvenly | ||
| SpaceBy | アイテム間のスペース(後述) ※詳細は「特殊な配置2」を参照 | |
関数の構成
LazyColumn(縦に並べる)はLazyList関数を呼び出す構成になっています。
LazyList関数はLazyListScope DSLを実行するエンジンです。コマンドに従ってアイテムを並べる処理(レイアウトフェーズで配置)を行います。
横に並べる処理の原理も同じであるため、LazyRow(横に並べる)も同じエンジンを用いています。ただし、LazyRowはフラグが異なります(isVertical = false)。
@Composable
fun LazyColumn(
    ...
    content: LazyListScope.() -> Unit
) {
    LazyList(
        modifier = modifier,
        state = state,
        contentPadding = contentPadding,
        flingBehavior = flingBehavior,
        horizontalAlignment = horizontalAlignment,
        verticalArrangement = verticalArrangement,
        isVertical = true,
        reverseLayout = reverseLayout,
        userScrollEnabled = userScrollEnabled,
        content = content
    )
}
@Composable
fun LazyRow(
    ...
    content: LazyListScope.() -> Unit
) {
    LazyList(
        modifier = modifier,
        state = state,
        contentPadding = contentPadding,
        verticalAlignment = verticalAlignment,
        horizontalArrangement = horizontalArrangement,
        isVertical = false,
        flingBehavior = flingBehavior,
        reverseLayout = reverseLayout,
        userScrollEnabled = userScrollEnabled,
        content = content
    )
}
リストの実装
リストの実装は、「LazyListScope DSL」を用いて行います。
実装方法
LazyColumn関数の最後の引数contentは、LazyListScopeレシーバーを持つスロットになっています。
ここにDSLコマンドを使って、アイテムの並びを記述します。

「記述順=並び順」になるので、直感的に判り易いと思います。
LazyListScope DSL
表に挙げるDSLコマンドが用意されています。
| コマンド | 概要 | |
|---|---|---|
| item( ) | 標準関数 | 単体のアイテムを追加 | 
| items(count: Int) | カウント値のアイテムを追加 | |
| stickyHeader( ) | リストの先頭に固定されるヘッダーを追加 | |
| items(items: Array | 拡張関数 | Arrayで列挙されたアイテムを追加 | 
| items(items: List | Listで列挙されたアイテムを追加 | |
| itemsIndexed(items: Array | Arrayで列挙されたIndex付きアイテムを追加 | |
| itemsIndexed(items: List | Listで列挙されたIndex付きアイテムを追加 | |
拡張関数のitemsは最終的に標準関数のitemsに落とし込まれて処理されます。
inline fun <T> LazyListScope.items(
    items: List<T>,
    noinline key: ((item: T) -> Any)? = null,
    noinline contentType: (item: T) -> Any? = { null },
    crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(
    count = items.size,
    key = if (key != null) { index: Int -> key(items[index]) } else null,
    contentType = { index: Int -> contentType(items[index]) }
) {
    itemContent(items[it])
}
※各DSLの使い方は「Compose LazyColumn:LazyListScope DSL」を参照
リストの配置
「表示可能な範囲(LazyColumnのサイズ)>リスト(コンテンツ)のサイズ」の時、verticalArrangementとhorizontalAlignmentを使って、リストの配置を指定できます。
図は代表して3箇所の配置を示しています。表示可能な箇所は全部で9カ所あります。
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            // Vertical
            verticalArrangement = Arrangement.Top,		// デフォルト
        //    verticalArrangement = Arrangement.Center,
        //    verticalArrangement = Arrangement.Bottom,
            // Horizontal
            horizontalAlignment = Alignment.Start,		// デフォルト
        //    horizontalAlignment = Alignment.CenterHorizontally,
        //    horizontalAlignment = Alignment.End,
        ) {
            items(5) {
                Box(
                    modifier = Modifier
                        .size(80.dp, 40.dp)
                        .background(_containerColor),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = "item %02d".format(it))
                }
            }
        }

特殊な配置1(空きスペースの均一化)
アイテムを配置した時に生まれる空きスペースを均一化することが出来ます。
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            // Vertical
            verticalArrangement = Arrangement.SpaceAround,
        //    verticalArrangement = Arrangement.SpaceBetween,
        //    verticalArrangement = Arrangement.SpaceEvenly,
            // Horizontal
            horizontalAlignment = Alignment.Start,      // デフォルト
        //    horizontalAlignment = Alignment.CenterHorizontally,
        //    horizontalAlignment = Alignment.End,
        ) {
            items(3) {
                Box(
                    modifier = Modifier
                        .size(80.dp, 40.dp)
                        .background(_containerColor),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = "item %02d".format(it))
                }
            }
        }

特殊な配置2(アイテム間のスペース)
隣り合うアイテム間にスペースを設けることが出来ます。
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            // Vertical
            verticalArrangement = Arrangement.spacedBy(2.dp),
        //    verticalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.CenterVertically),
        //    verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.Bottom),
            // Horizontal
            horizontalAlignment = Alignment.Start,      // デフォルト
        //    horizontalAlignment = Alignment.CenterHorizontally,
        //    horizontalAlignment = Alignment.End,
        ) {
            items(3) {
                Box(
                    modifier = Modifier
                        .size(80.dp, 40.dp)
                        .background(_containerColor),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = "item %02d".format(it))
                }
            }
        }

アイテムの並びを反転(reverseLayout)
reverseLayout引数は、アイテムの並びを反転させます。


並びが反転するだけでなく、配置の開始も下からになります。
スクロールの状態を監視・制御(state)
LazyListStateはスクロールの状態を監視・制御するクラスです。引数stateに指定します。
デフォルトはremenberLazyListState( )になっているので、監視・制御の必要がなければ、指定を省略できます。
LazyListStateのサンプル
以下は、リストの先頭へ移動するボタンを、スクロールの状況に合わせて表示・非表示するサンプルです。
        val _listState = rememberLazyListState()
        val _coroutineScope = rememberCoroutineScope()
        val _showButton = remember {
            derivedStateOf { _listState.firstVisibleItemIndex > 3 }
			//               ↑↑ 先頭アイテムのインデックス ↑↑
        }
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            state = _listState
        ) {
            items(24) {
                ItemBox { Text(text = "item %02d".format(it)) }
            }
        }
        AnimatedVisibility(  // ButtonのVisibilityをOn/Off
            modifier = Modifier.align(Alignment.BottomEnd).padding(10.dp),
            visible = _showButton.value
        ) {
            Button(
                onClick = {
                    _coroutineScope.launch {
                        _listState.scrollToItem(index = 0)
					//  ↑↑  リストの先頭へスクロール  ↑↑
                    }
                }
            ) { Text(text = "Top") }
        }
LazyListState#firstVisibleItemIndexは先頭アイテムのインデックスです。このインデックスの大小を用いて、ボタンのVisibilityを切り替えています。
LazyListState#scrollToItem( )は指定したアイテムへスクロールで移動します。
スクロールの動きを定義(flingBehavior)
FlingBehaviorはスクロールの動きを定義するクラスです。引数flingBehaviorに指定します。
デフォルトはScrollableDefaults.flingBehavior( )になっているので、変更の必要がなければ、指定を省略できます。
FlingBehaviorのサンプル
以下は、スクロールの停止位置を表示範囲の中心へスナップ(ぴったりと入る)させるサンプルです。
            val _listState = rememberLazyListState()
            val _flingBehavior = rememberSnapFlingBehavior(
                lazyListState = _listState
            )  // ↑↑ foundation 1.3.0で追加 ↑↑
            LazyColumn(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                state = _listState,
                flingBehavior = _flingBehavior
            ) {
                items(100) {
                    ItemBox { Text(text = "item %02d".format(it)) }
                }
            }
ライブラリで提供される既成の動きは2つのみです。
ScrollableDefaults.flingBehavior( )は、フリングの強さに合わせてスクロール距離が変化する動きです(動画の左)。
rememberSnapFlingBehavior( )は、スクロールの停止位置をリストの中心へスナップさせます(動画の右)。
ちなみに、次期APIの「foundation 1.7.0」は、スナップする停止位置を表示範囲の先頭(Start)もしくは末尾(End)にできます。
            val _listState = rememberLazyListState()
            val _flingBehavior = rememberSnapFlingBehavior(
                lazyListState = _listState,
            //    snapPosition = SnapPosition.Start
            //    snapPosition = SnapPosition.Center // デフォルト
                snapPosition = SnapPosition.End
            )  // ↑↑ foundation 1.7.0で追加 ↑↑
            LazyColumn(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                state = _listState,
                flingBehavior = _flingBehavior
            ) {
                items(100) {
                    ItemBox { Text(text = "item %02d".format(it)) }
                }
            }
関連記事:
