実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

Flutterアプリのリスト表示の基礎・ListView

はじめに

本記事ではFlutterのListViewについてポイントを見ていきます。まずは準備運動ということで、基本的な出力の仕方にフォーカスをあてます。

実際に動くListViewに関しては以後の記事で扱っていきます。

ListViewというと、スマホアプリの中でも最頻出GUIパーツではないでしょうか。TODOであればやることのリスト、動画再生アプリであれば動画のリスト、ニュースアプリであればニュースのリスト、メールアプリであれば受信箱リスト等など。

なにかしらデータが並んでいるところから選択して次のアクションをしていくというのは避けるのが難しいくらい基本ですから、ここを基礎からガッチリ抑えておけば半分アプリはできたようなものですよね。





ListViewの基本形

ListViewは縦、あるいは横に並んだ要素の表示を行うものです。スマートフォンでは大定番のUIですよね。

画面に要素が収まらない場合はスクロールバーが自動的につきます。スクロールバーが着く点がColumnとの大きな違いです。

サンプルのベースとなるコードを以下に示します。基盤となるウィジェットはMaterialApp/Scaffoldとしています:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home:Scaffold(
        //(1) AppBar表現
        appBar: AppBar(
          title: const Text("Flutter App"),
          actions: const [
            Padding(
               padding: EdgeInsets.only(right:20),
               child:  Icon(Icons.info)
            )
          ]
        ),
        //(2) ListViewの実装
        body:ListView(
            children: const [
              Text("Hello"),
              Text("Flutter"),
              Text("World"),
              Text("Hello"),
              Text("Flutter"),
              Text("World"),
            ]
        )
      )
    );
  }
}

これを実行した結果は以下になります:

f:id:linkedsort:20211017012529p:plain:w400

コードのポイント

(1) AppBar表現

AppBarは今回の主役ではないので見た目だけ軽くそれっぽくしてあります:

          actions: const [
            Padding(
               padding: EdgeInsets.only(right:20),
               child:  Icon(Icons.info)
            )
          ]

このactions属性のころに、少し右側にスペースを開けてinfoのアイコンを出しています。画面右上の丸に「i」のマークです。一旦見た目だけでアクションの実装はまた今度にしましょう。

(2) ListViewの実装

ListViewの基本形はColumnウィジェットと同様、childrenにウィジェットのリストを与えるものです。この時点でサンプルコードで「ListView」のところを「Column」に変えても表示は代わりません

ただし要素が多くて一画面に収まらない場合、ListViewであればスクロールバーが出てきて上下にスクロールさせられます。

まずは、リスト要素に対してListViewを使うだけなら特別なことはなにもない、というわけですね。



ListView.builderによるリスト構築

ListViewの作り方はいくつかありますが、もう一つの定番手法がListView.builderという名前付きコンストラクタを用いるものです。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    //(1) ここでリストの要素を作成
    var itemList = List<Text>.generate(50, (i)=>Text("Item"+i.toString()));

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home:Scaffold(
        appBar: AppBar(
          title: const Text("Flutter App"),
          actions: const [
            Padding(
               padding: EdgeInsets.only(right:20),
               child:  Icon(Icons.info)
            )
          ]
        ),
        //(2) ListView.builderによるリストビュー生成
        body:ListView.builder(
          itemCount: itemList.length,
          itemBuilder: (context, i)=>itemList[i],
        )
      )
    );
  }
}

これを実行した結果は以下になります:

f:id:linkedsort:20211017015733p:plain:w400

リストの長さが50にもなるため、リスト表示をタップするとスクロールバーが表示されることが分かると思います。

コードのポイント

(1) ここでリストの要素を作成

itemListというリストに「Item(数字)」というTextウィジェットが50個連なる要素を入れています。

    var itemList = List<Text>.generate(50, (i)=>Text("Item"+i.toString()));

このList.generateという名前付きコンストラクタは第1引数が繰り返し回数、第2引数がコールバック関数になっています。

このコールバック関数の引数として繰り返しのインデックスをとって繰り返し要素生成の処理を実行できるというわけです。便利ですね。

ここでは擬似的にデータを生成していますが、実際のアプリでは何らかのデータのリストが存在している想定です。

(2) ListView.builderによるリストビュー生成
    body:ListView.builder(
      itemCount: itemList.length,
      itemBuilder: (context, i)=>itemList[i],
    )

ListView.builderコンストラクタはitemCountで指定された数、itemBuilderで与えた関数を実行します。

この関数の引数はcontextと0からカウントアップされていくインデックスになっています。contextの方は一旦おいておいて、0からカウントアップされていくインデックスiを使ってリストのi番目の要素を作っていきます

ここでは単純にi番目の項目はitemListのi番の要素、ということでこれを返しています。

Flutterの公式ドキュメントでは、長いリストの場合このbuilderを使ったほうが効率的であるとしています。これは前出のリストを全部渡してListViewを生成する方法と違って、表示に必要のない範囲のウィジェットをbuildすることを避けられるためです。なので基本的にこちらを使っていきます。


CardウィジェットとListTileウィジェット

表示の基本がわかったところで、次は少し見栄えを良くしていきます

簡単にListViewの見栄えを良くする方法として、CardとListTileを活用する方法があります。

CardウィジェットはContainerのように囲みを作るもので、まさにカードっぽい見た目を作れます。ListViewの場合、各要素になにか囲みがないと中身の文字が境目なく並んでしまう形になりますので、なんらかで囲むのが普通です。そこで便利なのがCardウィジェットです。Containerと違ってデフォルトでほんのり影や丸角がついていてお手軽です。

ListTitleウィジェットはアイコンあるいは画像と、タイトル文字列、説明文字列という典型的なリストビューの要素を提供するものです。簡単な分、カスタマイズ性が低いウィジェットですが8割くらいのリストビューの用途ではそのまま使えそうです。

ListTileの主な属性値は下記の通りです:

属性 意味
leading 左に表示されるアイコン。画像やIconなどのウィジェットを指定
title 要素のタイトルとなるテキスト
subtitle 要素の説明にあたるサブテキスト
trailing 右に表示されるアイコン
isThreeLine 真にするとリスト要素を立てに拡げる
dense 真にすると縦のスペースを詰めて、リストの1要素の高さを削減
enabled 偽にすると選択不能な要素になり、表示が薄くなる
selected 選択されている要素であることを示す、表示が強調される
onTap タップされたときの処理を表すコールバック関数指定
onLongPress 長押しされたときの処理を表すコールバック関数指定

以下にサンプルコードを示します。今回は部分コードではなくmain.dart全体を示します:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var itemList = makeItems();

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home:Scaffold(
        appBar: AppBar(
          title: const Text("Flutter App"),
          actions: const [
            Padding(
               padding: EdgeInsets.only(right:20),
               child:  Icon(Icons.info)
            )
          ]
        ),
        body:ListView.builder(
          itemCount: itemList.length,
          itemBuilder: (context, i)=>itemList[i],
        )
      )
    );
  }

  List<Widget> makeItems(){
    List<Widget> retVal = [
      Card(
        child: ListTile(
          leading: const Icon(Icons.access_alarm),
          title: const Text('朝食'),
          subtitle: const Text('朝ごはんはとても大事'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: false,
          dense: false,
          enabled: true,
          selected: false,
          onTap: (){},
          onLongPress: (){},
        ),
      ),     
      Card(
        child: ListTile(
          leading: const Icon(Icons.wb_sunny),
          title: const Text('昼食'),
          subtitle: const Text('午後の仕事に弾みをつけるパワーランチ'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: false,
          dense: false,
          enabled: true,
          selected: false,
          onTap: (){},
          onLongPress: (){},
        ),
      ),      
      Card(
        child: ListTile(
          leading: const Icon(Icons.warning),
          title: const Text('おやつ'),
          subtitle: const Text('ダイエット中なのでおやつは選択できません'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: false,
          dense: false,
          enabled: false,
          selected: false,
          onTap: (){},
          onLongPress: (){},
        ),
      ),      
      Card(
        child: ListTile(
          leading: const Icon(Icons.wb_twighlight),
          title: const Text('夕食'),
          subtitle: const Text('おやつを食べるくらいなら早めの夕食を選択'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: false,
          dense: false,
          enabled: true,
          selected: true,
          onTap: (){},
          onLongPress: (){},
        ),
      ),      
      Card(
        child: ListTile(
          leading: const Icon(Icons.bedtime),
          title: const Text('夜食'),
          subtitle: const Text('しかし夕食が早すぎると夜にお腹が減る'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: false,
          dense: false,
          enabled: true,
          selected: false,
          onTap: (){},
          onLongPress: (){},
        ),
      ),
      const Padding(
        //隙間を開けるために挟みました
        padding: EdgeInsets.all(20),
      ),
      Card(
        child: ListTile(
          leading: const Icon(Icons.zoom_out_map),
          title: const Text('3行表示'),
          subtitle: const Text('3行表示を真にすると3行分のスペースが確保され説明文がその分長く表示されます'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: true,
          dense: false,
          enabled: true,
          selected: false,
          onTap: (){},
          onLongPress: (){},
        ),
      ),      
      Card(
        child: ListTile(
          leading: const Icon(Icons.android),
          title: const Text('高密度表示'),
          subtitle: const Text('高密度を指定すると、縦のスペースが詰まって高密度に表示できます'),
          trailing: const Icon(Icons.more_vert),
          isThreeLine: false,
          dense: true,
          enabled: true,
          selected: false,
          onTap: (){},
          onLongPress: (){},
        ),
      ),
    ];
    return retVal;
  }
}

これを実行した結果は以下になります:

f:id:linkedsort:20211017141539p:plain:w400

CardとListTileの利用で大幅に見た目が改善しましたね。それぞれの属性値の真偽・設定によって見た目がどう変わるか、コードと出力を見比べてみてください。

アイコンの色、テキストの色、選択時の色などは任意に指定可能です。上記の例はすべてデフォルトの色設定にしています。






おわりに

今回はリストの表示に絞ってポイントをまとめました。

見た目を凝っていくにはListViewの各項目に任意のウィジェットを載せられますので、自由にContainerから様々な要素をモリモリに乗っけていくことができます。

ただ当分は最後に紹介したCardウィジェットとListTileウィジェットでそこそこ格好いい見た目が作れますので、これを使って次回以降、リストの動きを作っていきます。


f:id:linkedsort:20211113124106j:plain