RSS2.0

Flutter ver 0.3.2 の、無限スクロールする GridView のサンプル

『The Infinite ListView』等で Google 検索すると無限スクロールする ListView については情報がえられるのですが、同じようなことを GridView でやっているサンプルは見つからなかったので書いておきます。言うならば、The Infinite GridView のサンプルです。Flutter は ver 0.3.2 を利用しています。
infinite_scroll_gridview.png
無限スクロールに関しては、やっていることは無限スクロール ListView と同じです。なので GridView のサンプルではありますが、同じ方法論で無限スクロール ListView も作ることができます。

機能としては、以下を実装しています。
・GridView を末尾までスクロールすると、次のデータを新たに作成して追加表示する
・GridView の要素は、表示されるタイミングで構築(遅延構築)される
・GridView の要素は 1 列で 3 個ずつ並べられる

以下がサンプルコードになります。
import 'package:flutter/material.dart';

void main() => runApp(new InfiniteGridViewSampleWidget());

/// 無限にスクロールする GridView サンプルの StatefulWidget
class InfiniteGridViewSampleWidget extends StatefulWidget {
  @override
  InfiniteGridViewSampleState createState() =>
      new InfiniteGridViewSampleState();
}

/// 無限にスクロールする GridView サンプルの State
class InfiniteGridViewSampleState extends State<InfiniteGridViewSampleWidget> {
  // 表示するデータの List。
  // 初期値としてデータを 20 件いれておく。
  final List<int> items = new List.generate(20, (index) => index);

  // GridView を構築して返す。
  // この GridView を末尾までスクロールした場合、表示するデータ件数を増やして追加表示できるようにする。
  //
  // GridView が末尾までスクロールされたかは、GridView に設定した ScrollController によって検知できる。
  //
  // GridView に表示される要素の最大数は、GridView が構築された時点で固定される。
  // そのため、表示するデータ件数を増やすには、再度 GridView を再構築する必要がある。
  @override
  Widget build(BuildContext context) {
    // GridView のスクロールを検知するための ScrollController。
    final ScrollController _scrollController = new ScrollController();
    // ScrollController にイベントリスナーを設定する。
    _scrollController.addListener(() {
      // 最後までスクロールしたら、次のデータを読み込む。
      if (_scrollController.position.maxScrollExtent <=
          _scrollController.position.pixels) {
        // 表示するデータを追加し、ウィジェットを再構築するよう通知する。
        setState(() {
          // 表示するデータにさらに 20 件データを追加する。
          this.items.addAll(
              new List.generate(20, (index) => this.items.length + index));
        });
      }
    });

    // GridView の要素を表示されるタイミングで構築できるように itemBuilder を指定する。
    return new MaterialApp(
      home: new Scaffold(
        // itemBuilder を指定できる GridView.builder() で GridView を構築する。
        body: GridView.builder(
          itemBuilder: (BuildContext context, int index) {
            // itemBuilder は、引数 index の位置にある GridView の要素が表示されるタイミングで
            // 呼び出され、その要素を構築して返す。

            print("make item: ${index}");

            // 引数 index の位置にある GridView の要素を構築して返す。
            return new Center(child: new Text("#${index}"));
          },
          // GridView で表示するデータの件数を設定する。
          // これ以上の件数のデータを表示する場合、新たな itemCount 値を設定して GridView を再構築する必要がある。
          itemCount: items.length,
          // スクロールされたことを検知するため、ScrollController を設定する。
          controller: _scrollController,
          // GridView の要素は横に 3 個ずつ並べてレイアウトする。
          // これは、new GridView.count(crossAxisCount: 3, children: <Widget>[]) に相当する。
          // GridView の要素の配置は SliverGridDelegateWithFixedCrossAxisCount を設定することで指定できる。
          gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
          ),
        ),
      ),
    );
  }
}

GridView のスクロールイベントを検知する

GridView が末尾までスクロールされた時に、追加で表示するデータを読み込むものとすると、GridView が末尾までスクロールされたことを検知する必要があります。

スクロールされたというイベントを検知するには、GridView に ScrollController を設定し、その ScrollController にスクロールイベントが発生した時に行う処理を追加しておきます。
// GridView のスクロールを検知するための ScrollController を作る。
final ScrollController _scrollController = new ScrollController();

// ScrollController に、スクロール時の処理をイベントリスナーとして設定する。
_scrollController.addListener(() {
  // 最後までスクロールしたら、次のデータを読み込む。
  if (_scrollController.position.maxScrollExtent <=
      _scrollController.position.pixels) {
    // 表示するデータを追加し、ウィジェットを再構築するよう通知する。
    setState(() {
      // 表示するデータにさらに 20 件データを追加する。
      this.items.addAll(
          new List.generate(20, (index) => this.items.length + index));
    });
  }
});
今回は GridView が末尾までスクロールされた場合に表示するデータを増やす必要があります。なので ScrollController に渡すリスナーの処理では、position 値の示すスクロール位置から末尾までスクロールされたかを判定し、その後表示するデータに新しいデータを追加しています。
後述しますが、データ件数を増やした後には GridView の再構築が必要になるので、setState() を呼び出しておきます。

この ScrollController を GridView.builder() の引数 controller に渡します。
GridView.builder(
  // スクロールされたことを検知するため、ScrollController を設定する。
  controller: _scrollController
);

GridView の要素を表示されるタイミングで構築(遅延構築)する

無限スクロール GridView のような巨大なウィジェットを構築する場合、そのすべての要素を最初に作っておくことはパフォーマンス的に現実的ではありません。GridView の領域で、各要素がはじめて表示されるタイミングでその要素を構築するというような、遅延構築をしていきます。
GridView で要素を遅延構築するには、ListView と同じく、new ListView.builder() の itemBuilder 引数に相当する設定をしていきます。

ただし、GridView の公式ドキュメントに記載されているサンプルでは GridView.count() が使われていますが、これには itemBuilder 引数がありません。そのため、itemBuilder 引数のある GridView.builder() を使って GridView を構築します。
GridView.builder(
  itemBuilder: (BuildContext context, int index) {
    // 引数 index の位置にある GridView の要素を構築して返す。
    return new Center(child: new Text("#${index}"));
  },
  // GridView で表示するデータの件数を設定する。
  // これ以上の件数のデータを表示する場合、新たな itemCount 値を設定して GridView を再構築する必要がある。
  itemCount: items.length,
);
ここでは itemBuilder 値に、引数 index の位置にある GridView の要素として Text ウィジェットを構築する無名関数を渡しています。GridView のデータのうち引数 index の位置の要素が表示される時にはじめて呼び出されるので、例えばまったくスクロールされていない GridView に要素が 12 個しか表示されないとすると、引数 index は 0 〜 11 の範囲しかとりません。それ以降の要素については、GridView がスクロールされて要素が表示されるタイミングに初めて呼び出されることになります。

また、GridView はあらかじめ表示する要素数を GridView.builder() の引数 itemCount 値として設定しておく必要があります。この itemCount 値以降の要素は存在しないことになるので、itemBuilder 値の処理も呼び出されません。
GridView を無限スクロールさせるには、データ件数を増やしてこの itemCount 値を更新しなければなりませんが、一度構築された GridView の itemCount 値は変更できないので、データ件数を増やすには GridView 自体を新しく作り直す必要があります。そのため、データ件数を増やした場合にはさきほどのように setState() で再構築の要求を通知する必要があります。

itemBuilder 値を指定しつつ GridView の要素を 3 つずつ配置する

一般的な GridView.count() を使ったサンプルでは、横に並べる要素数を引数 crossAxisCount に指定しています。しかし、引数 itemBuilder を指定できる GridView.builder() には引数 crossAxisCount がないため、代わりに gridDelegate 引数に対して横並びさせる要素数を指定した SliverGridDelegateWithFixedCrossAxisCount を渡してやる必要があります。
GridView.builder(
  // crossAxisCount 値を設定した SliverGridDelegateWithFixedCrossAxisCount を GridView.builder() に渡す
  gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
  ),
);
引数 gridDelegate に渡したインスタンスは GridView 内の要素をどのように配置するかを管理するもので、SliverGridDelegateWithFixedCrossAxisCount クラスは GridView.count() と同じく crossAxisCount 値の数ずつ要素を横並びに配置してくれます。

最後に

今回の無限スクロール GridView では非同期処理を使わずにデータの追加を行っていますが、ネットワーク経由でデータを取りたい時などは Future を組み合わせることになります。FutureBuilder の個ウィジェットとして GridView を位置づけ、GridView 自体を非同期処理にあわせて作りなおす、というのが一般的な例でしょう。もう 1 つのアプローチとしては、今回の例でもデータの追加とウィジェットの再描画が切り離されているのを利用して、純粋に Future.then() にてデータを増やしつつ setState() するというのもいいかもしれません。このあたりはまだ私も使い込んでいないので、時が来たらいろいろ試してみようと思います。

  FlutterDart  コメント (0)  2018/05/17 19:11:40


公開範囲:
プロフィール HN: ももかん
ゲーム作ったり雑談書いたり・・・していた時期が私にもありました。
カレンダー
<<2018, 5>>
293012345
6789101112
13141516171819
20212223242526
272829303112