FlutterでListView

# はじめに
今作ろうと思ってるアプリがあって最近Flutterを勉強しているのですが、その中でリストを生成する必要があるので、やり方を勉強しようと思います。

# 環境
Android Studio
Flutter 1.0

# とりあえずベース部分を作る
`State<T>`前まで一気に作ってしまいます。

``` Dart
import 'package:flutter/material.dart';

void main() => runApp(RootWidget());

class RootWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeWidget(),
    );
  }
}

class HomeWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return ListState();
  }
}
```

# 簡単なリストを作成してみる
リストには`ListView.builder`があり、これを実装すればとりあえずリストになってくれます。
プロパティの`itemBuilder`で`IndexedWidgetBuilder = Widget Function(BuildContext context, int index);`となるよう実装すれば良いようです。
とりあえず`index`がそのまま使えそうなので、それを表示するリストをまず作成します。

``` Dart
class ListState extends State<HomeWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("List Test"),),
      body: ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return Text(index.toString());
          }),
    );
  }
}
```

すごくしょっぱいですが、これで`index`の文字列がリストとなってくれました。
しかし、スクロールすると分かるのですが、永遠とスクロールできてしまいます。

# リストの数を制限する
`ListView.builder`には`itemCount`プロパティがあり、これを指定することでリストの数を制限することができます。
ここでは固定の数字を与えて動作を確認してみたいと思います。

``` Dart
class ListState extends State<HomeWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("List Test"),),
      body: ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return Text(index.toString());
          },
          itemCount: 10,),
    );
  }
}
```

これで無事10個だけのリストが出来ました。

# Listを変数で表示してみる
状態を変化したときには変数で制御することになるので、Dartの`List<E>`を使用してテキストを表示させるように変更してみます。
`return`している`Widget`をリストから取り出すように変更しただけです。

``` Dart
class ListState extends State<HomeWidget> {
  var listItem = ["one", "two", "three"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List Test'),),
      body: ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return Text(listItem[index]);
          },
          itemCount: listItem.length,),
    );
  }
}
```

これで`List<String>`からリストを生成することができました。

# 流石にもうちょっと見た目をなんとかしたい
このリストを表示するだけでは流石に見た目が悲しいので、もう少し見た目をまともにします。
こういう場合はWidget Catalogを見て勉強するのでしょうが、数が多すぎて何が何だか分からないので、色んなサンプルを見ながら使ってみて実際の動きを見るのが一番早いと思います。

とりあえずリストといえば、1つ1つのアイテムの境目が分からないと苦しいです。
タップしてもらうのにも、そもそも視認性もわかれていないと見辛いです。

その際に使えそうな`Widget`を調べてみました。
1つは[Card](https://docs.flutter.io/flutter/material/Card-class.html)クラスです。

``` Dart
class ListState extends State<HomeWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List Test')),
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Card(
              child: Padding(
                  child: Text('$index', style: TextStyle(fontSize: 22.0),),
                  padding: EdgeInsets.all(20.0),),
          );},
        itemCount: listItem.length,
      ),
    );
  }
}
```

<img width="288" alt="スクリーンショット 2019-02-05 8.42.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/71288/7a6d10ec-9c74-ade3-2d5a-c4b86b466346.png">

Themeの問題でみづらいですが、1つ1つのアイテムの境目が出来ました。
プラスして`Padding`したりしないと見た目が残念すぎて使い物にならないのですが、ベースの`Widget`として使えます。

もう1つは[ListTile](https://docs.flutter.io/flutter/material/ListTile-class.html)クラスです。
こちらはList用のタイルになっており、よくあるリストの見た目としたい場合はこちらを使う方が良いです。

<img width="285" alt="スクリーンショット 2019-02-05 21.19.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/71288/816f8848-5b76-9e8f-6747-2240e482b4df.png">

きちんとリストぽくするのに苦戦しましたが、よくあるリストを生成することができました。
何もしないと`ListTile`はListの枠線が表示されないため、`Container`の`decoration`を使用して枠線を表示しています。
ここらへんは大体セットで覚えておいた方が良さそうです。

``` Dart
class ListState extends State<HomeWidget> {
  var listItem = ['one', 'two', 'three'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List Test'),),
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Container(
              decoration: BoxDecoration(
                border: Border(
                  bottom: BorderSide(color: Colors.black38),
                ),
              ),
              child: ListTile(
                leading: const Icon(Icons.flight_land),
                title: Text('$index'),
                subtitle: Text('&listItem'),
                onTap: () { /* react to the tile being tapped */ },
            ));},
        itemCount: listItem.length,
      ),
    );
  }
}
```

ちなみに`Scaffold`の`backgroundColor`と`ListView`の色は独立しているので、裏の色とList内の色を変えるのは簡単に出来ます。

<img width="289" alt="スクリーンショット 2019-02-05 21.17.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/71288/7cf12fd8-974a-6cc6-0c37-77128a034348.png">


``` Dart
class ListState extends State<HomeWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List Test')),
      backgroundColor: Colors.purple,
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Card(
              child: Padding(
                  child: Text('$index', style: TextStyle(fontSize: 22.0),),
                  padding: EdgeInsets.all(20.0),),
          );},
        itemCount: listItem.length,
      ),
    );
  }
}
```

# ListViewを使わないでリストを作る
使い所は分かりませんが、`Column`を使って実現することも可能です。
流石に`Column`を全部書くのは長かったので、別functionに切り出して`List<Widget>`を作成するようにしています。
この中にある`MediaQuery.of(context)`は端末情報が取れるようなので、これで端末の横幅を取得してそれを`Column`の横幅に指定することでいい感じにしています。
ないと文字幅くらいまでしか`Widget`が生成されないので、リストっぽくなりません。

<img width="286" alt="スクリーンショット 2019-02-05 21.49.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/71288/bbb9b904-d672-79b5-6ef0-151ed54e85d8.png">


``` Dart
class ListState extends State<HomeWidget> {
  var listItem = ['one', 'two', 'three'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List Test'),),
      body: Column(
        children: createList(),
      ),
    );
  }

  List<Widget> createList() {
    final size = MediaQuery.of(context).size;
    return listItem.map((item) => Container (
      width: size.width,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(color: Colors.black38),
        ),
      ),
      child: Padding(
        child: Text('$item', style: TextStyle(fontSize: 22.0),),
        padding: EdgeInsets.all(20.0),),
    )).toList();
  }
}
```

# リストでPull to refreshを実現する
リストといったらPull to refreshだよねということで実現してみました。
Pull to refreshは簡単に実現できて、`RefreshIndicator`を`ListView`の親として実現すればOKです。
`RefreshIndicator`はプロパティとして`onRefresh`が指定可能となっているため、ここに`Future<void>`を戻り値として`Future`を実装し、その中で`setState`してあげればいい感じにrefreshしてくれます。

``` Dart
class ListState extends State<HomeWidget> {
  var listItem = [1, 2, 3];

  Future<void> _refresh() {
    return Future.sync(() {
      setState(() {
        listItem = listItem.map((item) => item + 1).toList();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List Test')),
      backgroundColor: Colors.purple,
      body: RefreshIndicator(
        child: ListView.builder(
          itemBuilder: (BuildContext context, int index) {
            return Card(
              child: Padding(
                child: Text(listItem[index].toString(), style: TextStyle(fontSize: 22.0),),
                padding: EdgeInsets.all(20.0),),
            );},
          itemCount: listItem.length,
        ),
        onRefresh: _refresh,
      ),
    );
  }
}
```

`Future`は非同期処理にすることもでき、その場合は`aync/await`で簡単に非同期処理とすることができます。
packageで非同期用のmoduleを取り込まないといけないと思いきや、`Future`であればいらないようです。

書き方は簡単でメソッドを`aync`付きで宣言して待ち合わせたい処理を`await`に続いて`Future`を実装すれば良いです。
あとはその下に非同期処理が完了したあとの処理を書きます。

`async`にすると`return`がいらなくなるので、消してしまってOKです。

今回は簡単なサンプルというコードで3秒待ってから状態を変化させるように変更してみます。

``` Dart
  Future<void> _refresh() async {
    await Future.delayed(Duration(seconds: 3));

    setState(() {
      listItem = listItem.map((item) => item + 1).toList();
    });
  }
```

# ListTileからModalを出してみる
Modal自体は`showDialog`を使用すれば簡単に実現可能となっています。
Modal内にListの情報を渡す方法については以下のような感じでするかしかないかなと思います。
トリガー自体は`onTap`なんかがありますが、引数が`void`でなければならないので、上手く情報を引き渡すことが出来ませんでした。
`AlertDialog`周りは専用のClassにしてしまえば情報引き渡せるので、共有のレイアウトを組むみたいなことは出来るので、そんなに困ることはないと思いますが、`showDialog`を共通化とまではいきませんでした。

``` Dart
  Widget _getCardChild() {
    if(station == null) {
      return Center(
        child: CircularProgressIndicator(),
      );
    } else {
      return ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Container(
            decoration: BoxDecoration(
              border: Border(
                bottom: BorderSide(color: Colors.black38),
              ),
            ),
            child: ListTile(
              title: Text(station.line[index].lineCd.toString()),
              subtitle: Text(station.line[index].lineName),
              onTap: () {
                showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return AlertDialog(
                        title: Text(station.line[index].lineCd.toString()),
                        content:  Text(station.line[index].lineName),
                        actions: <Widget>[
                          FlatButton(
                            child: Text("Close"),
                            onPressed: () {
                              Navigator.of(context).pop();
                            },
                          ),
                        ],
                      );
                    }
                );

              },
            ),
          );},
        itemCount: station.line.length,
      );
    }
  }
```

# おわりに
リストに関して色々見て来ました。
リストの中身を書き換えたいのあれば普通にトリガー作って`setState`するだけなので割愛しています。

やり始めた頃はわけがわからなかったのですが、進めて行くとFlutterの思想が見えてきて楽しくなってきました。
引き続き勉強していきたいです。