🙄

flutterの ListView.builderの注意点

2024/11/17に公開

ListView.builderの注意喚起

業務でflutterを使っていて、誰しもがリスト表示画面作成するかと思います。
その際に、ベストプラクティスではない方法で記載していました。

/// NG 例
SingleChildScrollView(
  child: Column(
    children: [
      Text('バッドパフォーマンス'),
      ListView.builder(
        shrinkWrap: true,
        physics: NeverScrollableScrollPhysics(),
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      ),
    ],
  ),
);

なぜ SingleChildScrollView はNGなのか?

1.パフォーマンスとメモリ効率
SingleChildScrollViewは、すべての子ウィジェットを一度に描画します。リストの項目が多い場合、全てをメモリに読み込むため、アプリが重くなったり、最悪の場合クラッシュすることもあります。

また、Column内に大量のウィジェットがあると、全てを一度にレンダリングするため、アプリが重くなります。

2.大量のデータに対応
リストの項目数が増えると、SingleChildScrollViewではパフォーマンスの低下が顕著になります。
全アイテムをメモリ上に保持するので、メモリ不足になる可能性があります。

結論としては少量のリスト表示ならSingleChildScrollViewを使ってもいいんじゃないのってこと。
(※その場合ListView.builderではなくfor文とかで表示する方がいいかも)

上記の解決策としては下記のように行うべし。

/// OK 例
class ListViewBuilderExample extends StatelessWidget {
  final List<String> items = List.generate(1000, (index) => 'アイテム $index');

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ListView.builderの例'),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      ),
    );
  }
}

ListView.builderの利点

1.パフォーマンスとメモリ効率
ListView.builderは、必要なときにだけアイテムを作成します。スクロールして見える範囲のアイテムのみを描画するため、メモリの使用量が抑えられ、アプリの動作がスムーズになります。

2.大量のデータに対応
ListView.builderは、アイテムを動的に生成・破棄するので、大量のデータでも効率的に扱えます。

3.コードのシンプルさ
ListView.builderを使うと、リストアイテムの生成方法を明確に定義できます。
アイテムのレイアウトやデータバインディングが簡単に行えるため、コードが読みやすくなります。

リストの他にも表示したいwidgetがある場合

ListView.builderの他にもテキストや画像を別で表示したい場合があります。
その際はどうすればいいのか?

CustomScrollViewとSliverListを使うべし

ベストプラクティスは下記になります。

class SliverExample extends StatelessWidget {
  final List<String> items = List.generate(1000, (index) => 'アイテム $index');

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sliverの例'),
      ),
      body: CustomScrollView(
        slivers: [
          SliverToBoxAdapter(
            child: Container(
              height: 200,
              color: Colors.blue,
              child: Center(child: Text('ヘッダーコンテンツ')),
            ),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                title: Text(items[index]),
              ),
              childCount: items.length,
            ),
          ),
        ],
      ),
    );
  }
}

SingleChildScrollViewとCustomScrollViewの違い

ListView.builderの他にもテキストや画像を別で表示したい場合はCustomScrollViewを使えばいいのか!でもListView.builderとSingleChildScrollView使ってないじゃん!と思いますよね。

まず、SingleChildScrollViewとCustomScrollViewの違いを説明します。

【SingleChildScrollView】

  • 単一の子ウィジェット(通常はColumnやRow)をスクロール可能にするためのウィジェットです。
  • 全ての子ウィジェットを一度にレンダリングするため、大量のデータを扱うとパフォーマンスが低下します。
  • コンテンツが少なく、スクロールが必要な場合に適しています。

【CustomScrollView】

  • スクロール可能な領域を作成し、複数のスリバー(スクロール可能なウィジェット)を組み合わせて柔軟なレイアウトを実現します。
  • 遅延レンダリングにより、大量のデータでも高いパフォーマンスを維持できます。(ListView.builderと同じ)
  • リスト、グリッド、固定ヘッダーなど、複数のスクロール可能な要素を組み合わせたい場合に適しています。
CustomScrollView(
  slivers: [
    SliverToBoxAdapter(
      child: Text('ヘッダーコンテンツ'),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
        childCount: items.length,
      ),
    ),
  ],
);

CustomScrollViewとListView.builderの組み合わせは避けるべき

CustomScrollViewとListView.builderを一緒に使えばいいじゃんと思いましたが、こちらは避けるべきです。

1.スクロールの競合
二重スクロールの問題: CustomScrollViewもListView.builderもスクロール可能なウィジェットです。これらをネストすると、スクロール操作が競合し、期待した動作にならない場合があります。

2.レイアウトの問題
サイズ制約の不一致: ListView.builderは無限の高さを持とうとしますが、CustomScrollView内ではサイズの制約が適切に伝わらず、レイアウトエラーが発生する可能性があります。

ベストプラクティスとしては、

import 'package:flutter/material.dart';

class CustomScrollViewExample extends StatelessWidget {
  final List<String> items = List.generate(1000, (index) => 'アイテム $index');

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomScrollViewの例'),
      ),
      body: CustomScrollView(
        slivers: [
          // 他のウィジェット(例:ヘッダー)
          SliverToBoxAdapter(
            child: Container(
              padding: EdgeInsets.all(16),
              child: Text(
                'ヘッダーコンテンツ',
                style: TextStyle(fontSize: 24),
              ),
            ),
          ),
          // リスト部分
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                return ListTile(
                  title: Text(items[index]),
                );
              },
              childCount: items.length,
            ),
          ),
          // 他のウィジェット(例:フッター)
          SliverToBoxAdapter(
            child: Container(
              padding: EdgeInsets.all(16),
              child: Text(
                'フッターコンテンツ',
                style: TextStyle(fontSize: 24),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

ListView.builderの代わりにSliverListとSliverChildBuilderDelegateを使用します。
CustomScrollView内で複数のSliverウィジェットを組み合わせることで、ヘッダーやフッター、グリッドなどを自由に配置でき、必要な部分だけをレンダリングするため、パフォーマンスが最適化されます。

ListView.builderのような役割をこなすwidgetが既に存在しています。

まとめ

用途でListView.builder、CustomScrollView、SingleChildScrollViewの使い分けができるとよいですよね。

ネットで調べてみたらNG例が多くて最初は信じて書いてましたが、規模が大きいアプリだとアプリが落ちたりします。パフォーマンスの向上はユーザー体験や、コストにも影響でてきますのでここを抑えておくかどうかは非常に大事かと。

僕自身曖昧だったので記事にしますた。

Discussion