[Flutter]初心者でもできる無限スクロールの実装方法

一覧表示画面で、スクロールすると次々にデータを読み込んでいく、無限スクロールを簡単に作る方法を紹介します。
無限スクロールを作る方法がいろいろあるのですが、初心者の自分でもできたのでこれが一番簡単かと思います。

## データを取得するAPIを準備する
今回はAPI経由でデータを取得し、一覧表示するのでまずAPIを準備します。
今回は、以下のswaggerで記載したtitleのみが一覧で返されるAPIを用意しました。次のデータがあれば、レスポンスのoffsetに次の位置が返り、なければ返りません。

```yml:sample.yml
openapi: 3.0.0
info:
  title: sample
  version: '1.0'
  description: sample
  contact:
    name: murapon
servers:
  - url: 'http://localhost:31180'
paths:
  /list:
    get:
      operationId: get-list
      summary: 一覧取得
      description: 一覧取得
      parameters:
        - schema:
            type: integer
            default: '10'
          in: query
          name: limit
          description: limit
        - schema:
            type: integer
            default: '0'
          in: query
          name: offset
          description: offset
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/response_list_get'
components:
  schemas:
    response_list_get:
      title: reponse_list_get
      description: 一覧取得APIのレスポンスモデル
      type: object
      properties:
        list:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
                description: タイトル
            required:
              - title
        total_count:
          type: integer
          description: 総件数
        next_offset:
          type: integer
          description: 次のoffset
      required:
        - list
        - total_count
```

## flutterでswaggerを読み込んで、APIからデータを取得できるようにする
[FlutterでSwagger(openapi-generator)を使う方法](https://qiita.com/murapon/items/2e0933a054b9555378e2)を参考にしてください。

## 一覧表示を作る
以下が、flutter側の全ソースです。

```dart:sample_page.dart
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:openapi/api.dart';

class SamplePage extends StatefulWidget {
  const SamplePage({
    Key key,
  }) : super(key: key);

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

class _SamplePageState extends State<SamplePage> {
  List<Map> list = null;
  bool loading = false;
  int offset = 0;

  @override
  Widget build(BuildContext context) {
    var length = list?.length ?? 0;
    return Scaffold(
      appBar: AppBar(title: Text("一覧")),
      body: new ListView.builder(
        itemBuilder: (context, index) {
          if (index == length && offset!=null) {
            _load();
            return new Center(
              child: new Container(
                margin: const EdgeInsets.only(top: 8.0),
                width: 32.0,
                height: 32.0,
                child: const CircularProgressIndicator(),
              ),
            );
          } else if (index > length) {
            return null;
          }
          if(list.length > index) {
            var event = list[index];
            return Container(
              child: ListTile(title: Text(event['title'])),
            );
          } else {
            return null;
          }
        },
      ),
    );
  }

  Future<void> _load() async {
    if (loading || offset == null) {
      return null;
    }
    loading = true;
    try {
      var client = ApiClient(basePath: "http://10.0.2.2:31180");
      var defaultApi = DefaultApi(client);
      int limit = 3; // 一度に取得する件数
      var response = await defaultApi.getListWithHttpInfo(
          limit:limit, offset: offset);
      ResponseListGet results =  ResponseListGet.fromJson(jsonDecode(response.body));
      setState(() {
        offset = results.nextOffset;
        if (list == null) {
          list = <Map>[];
        }
        results.list.forEach((dynamic item) {
          list
              .add({'title': item.title as String});
        });
      });

    } catch (e) {
      print(e.toString());
    } finally {
      loading = false;
    }
  }
}
```

最初の、

```dart:sample_page.dart（抜粋）
List<Map> list = null;
bool loading = false;
int offset = 0;
```

で、初期化し、`_load();`を実行します。
`_load();`の中で、APIからデータを取得し、`setState`を使って、取得したデータを`list`に入れつつ、再描画処理を実行し、初期表示を行います。
2回目は、取得した`offset`の値があれば、再度APIを実行しなければ、実行しません。
API経由で全データを取得し終わったら、それ以上スクロールしても何もしないという実装にしたかったので、`loading`と`offset`で制御しましたが、これを二つを消せば、何度も_load()を実行し続けることが可能です。_load()の中身を変えることで、全部表示したらまた最初から表示し直すなどの表示も可能です。