3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flutterで複数のListViewを組み合わせた画面を作成する

Last updated at Posted at 2020-04-29

はじめに

Flutterで、1つの画面内に複数のListViewやGridViewを並べたい時があったので、その際の記録を記述しています。
(GridViewを使った実装コード例はありません)

まとめ

Sliver使おう。

シンプルにListViewをネストし並べる

実装概要

簡単に実装して見るとしたら、ListViewの中にさらに複数のListView (ListView.physicsにNeverScrollableScrollPhysicsを指定しておく) という実装かなあ、と思います。

ListView(
  shrinkWrap: true,
  children: <Widget>[
    _ScrollView(label: '1st', baseColor: Colors.blue),
    _ScrollView(label: '2nd', baseColor: Colors.yellow),
    _ScrollView(label: '2nd', baseColor: Colors.yellow),
    ...
  ],
);

//_ScrollViewの中身
ListView.builder(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  itemCount: itemCount,
  itemBuilder: (_, int index) {
    final String label = '${widget.label}_item_$index';
    return _ListItem(..);
  },
)

親のListViewの代わりにSingleChildScrollViewとColumnの組み合わせで実装される方もいらっしゃると思います。

実装(コード)

サンプルコード全文は以下に置いときます。

コードサンプル

class Body extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      shrinkWrap: true,
      children: <Widget>[
        _ScrollView(
          key: const ValueKey<String>('ListView.children#1'),
          label: 'ListView.children#1',
          baseColor: Colors.blue,
        ),
        _ScrollView(
          key: const ValueKey<String>('ListView.children#2'),
          label: 'ListView.children#2',
          baseColor: Colors.yellow,
        ),
        _ScrollView(
          key: const ValueKey<String>('ListView.children#3'),
          label: 'ListView.children#3',
          baseColor: Colors.red,
        ),
        _ScrollView(
          key: const ValueKey<String>('ListView.children#4'),
          label: 'ListView.children#4',
          baseColor: Colors.orange,
        ),
        _ScrollView(
          key: const ValueKey<String>('ListView.children#5'),
          label: 'ListView.children#5',
          baseColor: Colors.green,
        ),
      ],
    );
  }
}

class _ScrollView extends StatefulWidget {
  const _ScrollView({
    Key key,
    this.label,
    this.baseColor,
  }) : super(key: key);
  final String label;
  final Color baseColor;

  @override
  __ScrollViewState createState() => __ScrollViewState();
}

class __ScrollViewState extends State<_ScrollView> {
  final itemCount = 50;

  @override
  Widget build(BuildContext context) {
    print('$runtimeType (${widget.key}) build');
    return ListView.builder(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      itemCount: itemCount,
      itemBuilder: (_, int index) {
        final String label = '${widget.label}_item_$index';
        return _ListItem(
          key: ValueKey<String>(label),
          child: Container(
            constraints: const BoxConstraints.expand(height: 48),
            color: widget.baseColor.withOpacity(1 / (index % 5 + 1)),
            child: Center(child: Text(label)),
          ),
        );
      },
    );
  }
}

class _ListItem extends StatelessWidget {
  const _ListItem({Key key, this.child}) : super(key: key);
  final Widget child;

  @override
  Widget build(BuildContext context) {
    print('$runtimeType ($key) build');
    return child;
  }
}

この実装での問題

(1)パフォーマンスがあまりよくないです。
(2)スクロールをして _ScrollView から次に続く _ScrollView が表示されようとした時、一瞬スクロールが引っかかります
(3)最上位のListView.childrenに指定した _ScrollView (実体はほぼListView) がビルドされたと同時に、 _ScrollView の中のListView.childrenのWidgetがビルドされてしまいます。

(2)については(3)のように「_ScrollViewがビルドされたと同時に_ScrollViewのListView.childrenのWidgetがビルド」されてしまうのが要因と思います。
特に(3)についてですが、ListViewの子要素はlazilyに作られるはずなのに、その恩恵を受けられていません。

また_ScrollViewのListView.childrenのWidgetのうち、画面外に移動したものでも、それらが破棄されることはありません。

どうするか

CustomScrollViewとSliverを使います。
ListViewに以下のようにあります。

Transitioning to CustomScrollView
A ListView is basically a CustomScrollView with a single SliverList in its CustomScrollView.slivers property.

If ListView is no longer sufficient, for example because the scroll view is to have both a list and a grid, or because the list is to be combined with a SliverAppBar, etc, it is straight-forward to port code from using ListView to using CustomScrollView directly.

これを読んでCustomScrollViewを使う方が良さそうだと思いました。(具体的な根拠なし)

ただCustomScrollView.sliverにはRenderSliverを生成するWidgetじゃなければダメとCustomScrollViewのドキュメントにあるので、

Widgets in these slivers must produce RenderSliver objects.

単にListViewをCustomScrollViewにするだけでは動かなそうです。

CustomScrollView + Sliverで作り直した例

実装修正・説明

修正後のコードはこの後の 「実装(Sliverを使用したコード)」 をご確認ください。
まず最上位のListViewをCustomScrollViewに置き換えます。

class BodyWithSliver extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      shrinkWrap: true,
      slivers: <Widget>[
        ...
      ],
    );
  }

_ScrollViewはどうしよう。。
とりあえずSliverListの継承クラスにしてやりきろうと思います。
SliverList.delegate にはSliverChildBuilderDelegateを使用しました。


class _ScrollViewSliver extends SliverList {
  _ScrollViewSliver({
    Key key,
    String label,
    Color baseColor,
    @required int itemCount,
  })  : assert(itemCount != null && itemCount > 0),
        super(
          key: key,
          delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
              final String _label = '${label}_item_$index';
              return _ListItem(
                key: ValueKey<String>(_label),
                child: Container(
                  constraints: const BoxConstraints.expand(height: 48),
                  color: baseColor.withOpacity(1 / (index % 5 + 1)),
                  child: Center(child: Text(_label)),
                ),
              );
            },
            childCount: itemCount,
            addSemanticIndexes: false,
          ),
        );
}

SliverChildListDelegateでも同じように作れました。
なお、SliverChildBuilderDelegateのドキュメントには

  • IndexedSemanticsでAnnotateすることが必要
  • 以下の内容より、1つのscroll view (多分CustomScrollViewのことだと思うが)に複数のdelagateがある場合はindexが正しくない

If multiple delegates are used in a single scroll view, then the indexes will not be correct by default.

という内容もありましたが、以下の理由から addSemanticIndexes: false としています。

  • IndexedSemanticsでAnnotateしてもらわなくても表示やスクロールには問題なさそう
  • IndexedSemanticsでAnnotateしてもらうとTalkOver/Voiceoverでスクロール位置に応じたお知らせをしてくれる(?)、という機能があるようですが、それは今は必要なし

これでほぼ実装完了で、ログを確認したところ、以下が解決できていました。

(2)スクロールをして_ScrollViewから次に続く_ScrollViewが表示され長とした時、一瞬スクロールが引っかかります
(3)最上位のListView.childrenに指定した _ScrollView (実体はほぼListView) がビルドされたと同時に、_ScrollViewのListView.childrenのWidgetがビルドされてしまいます。

パフォーマンスはどうなんだろう・・・いっぺんにビルドがされない(lazilyなビルドをする)ので、パフォーマンスは良くなっている、と思ってます。。

実装(Sliverを使用したコード)

サンプルコード全文は以下に置いときます。

コードサンプル

class BodyWithSliver extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      shrinkWrap: true,
      slivers: <Widget>[
        _ScrollViewSliver2(
          key: const ValueKey<String>('ListView.children#1'),
          label: 'ListView.children#1',
          baseColor: Colors.blue,
          itemCount: 50,
        ),
        _ScrollViewSliver(
          key: const ValueKey<String>('ListView.children#2'),
          label: 'ListView.children#2',
          baseColor: Colors.yellow,
          itemCount: 50,
        ),
        _ScrollViewSliver2(
          key: const ValueKey<String>('ListView.children#3'),
          label: 'ListView.children#3',
          baseColor: Colors.red,
          itemCount: 50,
        ),
        _ScrollViewSliver(
          key: const ValueKey<String>('ListView.children#4'),
          label: 'ListView.children#4',
          baseColor: Colors.orange,
          itemCount: 50,
        ),
        _ScrollViewSliver2(
          key: const ValueKey<String>('ListView.children#5'),
          label: 'ListView.children#5',
          baseColor: Colors.green,
          itemCount: 50,
        ),
      ],
    );
  }
}

/// SliverChildBuilderDelegateを使った例
class _ScrollViewSliver extends SliverList {
  _ScrollViewSliver({
    Key key,
    String label,
    Color baseColor,
    @required int itemCount,
  })  : assert(itemCount != null && itemCount > 0),
        super(
          key: key,
          delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
              final String _label = '${label}_item_$index';
              return _ListItem(
                key: ValueKey<String>(_label),
                child: Container(
                  constraints: const BoxConstraints.expand(height: 48),
                  color: baseColor.withOpacity(1 / (index % 5 + 1)),
                  child: Center(child: Text(_label)),
                ),
              );
            },
            childCount: itemCount,
            addSemanticIndexes: false,
          ),
        );
}

/// SliverChildListDelegateを使った例
class _ScrollViewSliver2 extends SliverList {
  _ScrollViewSliver2({
    Key key,
    String label,
    Color baseColor,
    @required int itemCount,
  })  : assert(itemCount != null && itemCount > 0),
        super(
          key: key,
          delegate: SliverChildListDelegate(
            List<Widget>.generate(itemCount, (int index) {
              final String _label = '${label}_item_$index';

              return _ListItem(
                key: ValueKey<String>(_label),
                child: Container(
                  constraints: const BoxConstraints.expand(height: 48),
                  color: baseColor.withOpacity(1 / (index % 5 + 1)),
                  child: Center(child: Text(_label)),
                ),
              );
            }),
            addSemanticIndexes: false,
          ),
        );
}

class _ListItem extends StatelessWidget {
  const _ListItem({Key key, this.child}) : super(key: key);
  final Widget child;

  @override
  Widget build(BuildContext context) {
    print('$runtimeType ($key) build');
    return child;
  }
}

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?