LazyColumn — Composeで効率的なリストとは

著者: IT Sectr 公開日: 2026-02-24 読了時間: 10 分

LazyColumn — 遅延ロードでスクロール可能なリストを表示するためのJetpack Composeコンポーネントです。RecyclerViewとは異なり、LazyColumnはViewHolderを作成せず、XMLも使用しません — すべての要素はcomposable関数を通じて宣言的に記述されます。LazyColumnとは何か、LazyListStateとitems APIの仕組み、60 FPSのパフォーマンスを達成する方法を解説します。IT Sectrでは、すべての新しいComposeプロジェクトでLazyColumnを使用しています。従来のアプローチとの比較については、RecyclerViewの記事をご覧ください。

重要なポイント

  • LazyColumn — 遅延ロードを備えた垂直リストのためのJetpack Composeコンポーネント、Material Design 3の一部。
  • LazyListState — リストの状態:スクロール位置、表示要素、プログラムによるスクロール。
  • items API — データバインディングのためのキー付きのitems()itemsIndexed()items()関数。
  • key — 要素の安定した識別のためのパラメータ。Composeがコンポジションを再利用できるようにします。
  • LazyVerticalGrid — 固定列数のグリッド用のLazyColumnバージョン。

Jetpack ComposeのLazyColumnとは?

LazyColumnはFoundation Composeライブラリのcomposable関数で、遅延ロードでアイテムの垂直リストを表示します:画面に表示されているか、その近く(プリフェッチゾーン)のアイテムのみが作成・コンポーズされます。LazyColumnはCompose 1.0(2021)でLazyRow(水平リスト)やLazyVerticalGrid(グリッド)とともに登場しました。

遅延ロードSubcomposeLayoutメカニズムを通じて機能します:LazyColumnが利用可能なスペースを測定し、LayoutInfoに表示範囲を要求し、その範囲内のアイテムのみをコンポーズします。画面外にスクロールされたアイテムはコンポジションを離れます(プリフェッチバッファを除く)。Google(Android Performance、2026)によると、LazyColumnはミッドレンジデバイスで10000+アイテムのリストをスクロールする際に60 FPSを維持します。

Column + ScrollStateとの違い

Columnと垂直スクロール — verticalScroll修飾子を適用した、すべてのアイテムを上下に並べる通常の配置です。Columnは遅延せず、表示されていなくてもすべての子要素が即座にコンポーズされます。200+アイテムのリストでは、Columnは起動時に大きなラグを引き起こします。LazyColumnは表示アイテム+バッファ(デフォルトでは前方/後方1画面)のみをコンポーズするため、長いリストに不可欠です。

items API:リストへのデータバインディング

items APIはLazyColumnの拡張関数セットで、データリストと各アイテムのDSL記述子を受け取ります。主要な関数は次のとおりです:固定数のitems(count, key, itemContent)、リスト用のitems(list, key, itemContent)、インデックス付きのitemsIndexed(list, key, itemContent)。keyパラメータはパフォーマンスに必須です — Composeに安定した要素識別子を提供します。

kotlin
// itemContentを使用した基本LazyColumn
@Composable
fun ArticleList(articles: List<Article>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Header — 遅延なし、常に先頭
        item {
            Text("最新記事", style = MaterialTheme.typography.headlineMedium)
        }

        // idによるキー付き記事リスト
        items(articles, key = { it.id }) { article ->
            ArticleCard(
                title = article.title,
                summary = article.summary,
                onClick = { onArticleClick(article.id) }
            )
        }

        // ローディングインジケータ付きFooter
        item {
            CircularProgressIndicator(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            )
        }
    }
}

// カスタムリストアイテム
@Composable
fun ArticleCard(title: String, summary: String, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
    ) {
        Column(modifier = Modifier.padding(12.dp)) {
            Text(text = title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = summary, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

item {} vs items():リストに常に存在する単一アイテム(ヘッダー、フッター、区切り線)にはitem {}を使用します。動的データリストにはitems()を使用します。items()はIterableまたはIntと、各要素に対して呼び出されるDSLブロックを受け取ります。1つのLazyColumnでitemとitemsを組み合わせます:呼び出しの順序が表示順序を決定します。

LazyListState:スクロール位置の管理

LazyListStateはリストの状態を格納するオブジェクトです:現在のスクロール位置(firstVisibleItemIndex、firstVisibleItemScrollOffset)、表示アイテム(layoutInfo)、プログラムによるスクロールメソッド(scrollToItem、animateScrollToItem)。LazyListStateはrememberLazyListState()で作成され、stateパラメータを通じてLazyColumnに渡されます。

kotlin
// スクロール位置保持付きLazyColumn
@Composable
fun ScrollingList(items: List<Item>) {
    val listState = rememberLazyListState()

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn(state = listState) {
            items(items, key = { it.id }) { item ->
                ListItemView(item = item)
            }
        }

        // "上に"ボタンは10番目の要素の後に表示
        val showButton by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 10 }
        }

        AnimatedVisibility(visible = showButton) {
            FloatingActionButton(
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(16.dp),
                onClick = {
                    coroutineScope.launch {
                        listState.animateScrollToItem(0)
                    }
                }
            ) {
                Icon(Icons.Default.KeyboardArrowUp, "トップへ")
            }
        }
    }
}

// 表示アイテム情報の取得
@Composable
fun ListDebugInfo(listState: LazyListState) {
    val visibleItems = listState.layoutInfo.visibleItemsInfo
    val totalItems = listState.layoutInfo.totalItemsCount
    val firstItem = visibleItems.firstOrNull()?.index
    val lastItem = visibleItems.lastOrNull()?.index

    Text("$totalItems中$firstItem–$lastItemを表示")
}

プログラムによるスクロールはlistStateのsuspend関数を介して実行されます:scrollToItem(index, scrollOffset) — 即時移動(アニメーションなし)、animateScrollToItem(index) — アニメーション付き。コルーチンからのスクロールには、LaunchedEffectまたはcoroutineScope.launchを使用します。重要:コンポジション内(@Composable内)でsuspend関数を呼び出さないでください — ラムダ(onClick、LaunchedEffect)を使用します。

LazyColumn vs RecyclerView:アプローチの比較

LazyColumnRecyclerViewは同じ問題を解決します — 長いリストの効率的な表示。違いはアーキテクチャにあります:RecyclerViewはViewHolderとAdapter(従来のAndroid View)を使用し、LazyColumnはXMLやViewHolderなしの宣言的なcomposable関数を使用します。選択はプロジェクトのテクノロジースタックに依存します:Compose vs ViewベースUI。

パラメータLazyColumn(Compose)RecyclerView(View)
レイアウトComposable関数(Kotlin DSL)XMLファイル + ViewBinding
アダプターitems() / itemsIndexed() DSLRecyclerView.Adapter + ViewHolder
ソート/フィルターSnapshot state + derivedStateOfDiffUtil + AsyncListDiffer
アニメーションAnimatedVisibility + Modifier.animateItemDefaultItemAnimator
異なるアイテムタイプitem {}/items() + タイプ別whengetItemViewType + 複数のViewHolder
位置へのスクロールlistState.animateScrollToItem()layoutManager.scrollToPosition()
プリフェッチインプロセス(SubcomposeLayoutバッファ)RecycledViewPool + GapWorker
ネットワーク/ページネーションcollectAsLazyPagingItems() + Paging 3PagingDataAdapter + Paging 3

Googleの推奨(Android Developers、2026):Jetpack Composeの新規プロジェクトではLazyColumnを使用してください。ViewベースのプロジェクトではRecyclerViewを使用してください。LazyColumnにはComposeが必要で、APKに約3–5 MB追加されます — 古いデバイスのサポート時に考慮してください。プロジェクトが既にViewシステムを使用している場合、LazyColumnはAndroidViewなしではXMLレイアウトと同じ画面に共存できません — そのような場合は、RecyclerViewを使い続ける方が簡単です。

LazyVerticalGridとLazyHorizontalGrid:グリッド

LazyVerticalGridは固定列数のグリッド用のLazyColumnバージョンです。LazyHorizontalGridは固定行数の水平グリッドです。両方のコンポーネントはLazyColumnと同じ遅延ロードの原則と同じitems APIを使用します。columnsパラメータはGridCells.Fixed(N)またはGridCells.Adaptive(minSize)で列数を決定します。

kotlin
// アダプティブ列のグリッド
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 128.dp),
        contentPadding = PaddingValues(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(photos, key = { it.id }) { photo ->
            AsyncImage(
                model = photo.url,
                contentDescription = photo.title,
                modifier = Modifier.aspectRatio(1f)
            )
        }
    }
}

// 水平リスト(LazyRow)
@Composable
fun CategoryCarousel(categories: List<Category>) {
    LazyRow(
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(categories, key = { it.id }) { category ->
            FilterChip(
                selected = category.isSelected,
                onClick = { onCategoryClick(category.id) },
                label = { Text(category.name) }
            )
        }
    }
}

GridCells.Adaptiveは最小セルサイズに基づいて自動的に列数を計算します。たとえば、GridCells.Adaptive(128.dp)は360 dp幅の画面で2列、600 dpのタブレットで4列を配置します。GridCells.Fixed(2)は常に正確に2列を表示します。Adaptiveはマルチスクリーン適応に推奨されます — メディアクエリなしで自動的に画面幅に調整されます。

LazyColumnのパフォーマンス:key、contentType、prefetch

LazyColumnのパフォーマンスは3つの要因に依存します:キーの安定性(key)、コンテンツタイプ(contentType)、プリフェッチバッファ(beyondBounds)。デフォルトでは、LazyColumnは前方1画面と後方1画面をバッファリングします。Google(Android Developers、2026)は、異なるアイテムタイプや複雑なコンポジションのリストに対してこれらのパラメータを設定することを推奨しています。

パラメータ説明推奨
key安定した要素識別子常にkey = { it.id }を指定してください。keyがないとComposeはインデックスを使用し、アイテムの並べ替えでアニメーションが壊れます。
contentTypeプール分離のためのコンテンツタイプ異なるアイテムタイプ(テキスト+画像+広告)のリストに指定します。Composeは同じcontentType内でのみコンポジションを再利用します。
beyondBoundsプリフェッチ画面数画像が多いリストでは2~3に増やします。デフォルトは1画面です。
Modifier.animateItem位置変更アニメーションアニメーション付きのソートとフィルタリングに使用します(Compose 1.7+)。
kotlin
// contentTypeとprefetchで最適化されたLazyColumn
@Composable
fun OptimizedFeed(items: List<FeedItem>) {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize(),
        beyondBoundsPageCount = 2  // 2画面のプリフェッチ
    ) {
        items(
            items = items,
            key = { it.id },
            contentType = { it.type }  // タイプによる分割
        ) { item ->
            when (item) {
                is FeedItem.Post -> PostView(item)
                is FeedItem.Ad -> AdView(item)
                is FeedItem.Suggested -> SuggestedView(item)
            }
        }
    }
}

// listStateによるパフォーマンス測定
@Composable
fun ScrollPerformance(listState: LazyListState) {
    val scrollInfo = listState.layoutInfo
    val visibleCount = scrollInfo.visibleItemsInfo.size
    val total = scrollInfo.totalItemsCount

    // derivedStateOf — 値が変更されていない場合は再コンポーズしない
    val scrollProgress by remember {
        derivedStateOf {
            if (total > 0) visibleCount.toFloat() / total else 0f
        }
    }

    LinearProgressIndicator(
        progress = scrollProgress,
        modifier = Modifier.fillMaxWidth().height(2.dp)
    )
}

よくある間違い:(1)keyの欠落 — リストが変更されるたびに要素が再コンポーズされます。(2)スナップショットフローなしのmutableStateListOfの使用 — 変更が追跡されない可能性があります。(3)itemContent内でのsuspend関数の呼び出し — コンポジションが中断されます。(4)混合タイプでのcontentTypeなし — Composeがテキスト投稿に広告のコンポジションを再利用し、視覚的な問題を引き起こします。IT Sectrでは、3つ以上のコンテンツタイプがあるすべてのリストにcontentTypeを追加しています。

よくある質問

LazyColumnのスクロールが遅い — どうすればいいですか?

3つのパラメータを確認してください:(1)key — 安定したキーがないとComposeはコンポジションを再利用できません。(2)contentType — 異なるアイテムタイプのリスト用。(3)beyondBoundsPageCount — 画像の場合は2に増やします。itemContent内の重い計算は、データによるキー付きのrememberに移動します。シンプルなグラフィックにはImageの代わりにModifier.drawWithContentを使用します。

LazyColumnのアイテム間に区切り線を追加するには?

アイテム間の間隔にはLazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp))を使用します。線のある視覚的な区切り線には、itemsの間にitem { Divider() }を追加します。自動区切り線:items(items, key = { it.id }) { item -> ... }、各要素の間にitem { HorizontalDivider() }を挿入します。これには、LazyListScope.items(list, key) { /* 要素 */ }と個別にLazyListScope.item { Divider() }を使用します。

Column内のLazyColumnが機能しない — なぜですか?

LazyColumnはデフォルトで無限の高さ(fillMaxSize)を持ちます。固定高さのないColumn内では、LazyColumnはサイズを決定できません。解決策:(1)LazyColumnに固定高さを設定:Modifier.height(400.dp)。(2)Column内でModifier.weight(1f)を使用。(3)LazyColumnを垂直スクロール可能なコンテナ内に入れ子にしない — ヘッダーとフッターにitem {}を使用してLazyColumnをルート要素として使用します。

データ変更時にLazyColumnを更新するには?

LazyColumnはStateの変更に自動的に反応します。データがmutableStateListOfまたはmutableStateOfに保存されている場合、Composeは変更された要素のみを再コンポーズします。ViewModelからのリストには、FlowとともにcollectAsState()を使用します。keyでリストを更新すると、ComposeはModifier.animateItemPlacement()を介して変更(追加、削除、並べ替え)を自動的にアニメーション化します。

LazyColumn vs LazyVerticalGrid — どちらを選ぶべきですか?

LazyColumn — 単一列リスト用(フィード、チャット、コメント)。LazyVerticalGrid — グリッド用(ギャラリー、カタログ、アイコン)。アイテムが電話では1列、タブレットでは2列になるべき場合 — GridCells.Adaptive付きのLazyVerticalGridを使用します。混合グリッドが必要な場合(1セクションで異なる列数) — LazyColumnを使用し、items内に水平LazyRowまたは入れ子のLazyVerticalGridを挿入します。

まとめ

  • LazyColumn — 宣言的APIを備えた遅延垂直リストのためのJetpack Composeコンポーネント。
  • items() APIはリストと各アイテムのDSLブロックを受け取ります。パフォーマンスにはkeyが必須です。
  • LazyListStateはスクロール位置を管理し、プログラムによるスクロールのためのsuspend関数を提供します。
  • contentTypeは異なるアイテムタイプのリストのコンポジションプールを分離します。
  • LazyVerticalGrid / LazyRow — 同じitems APIを持つグリッドと水平リストのバリエーション。
  • LazyColumnはSubcomposeLayoutとプリフェッチバッファのおかげで10000+アイテムで60 FPSを維持します。
  • 新しいComposeプロジェクトにはLazyColumnを使用。ViewベースのプロジェクトにはRecyclerView。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください