Flutter の Widget をコードを動かしながら学ぶ ListTile編

マテリアルデザインに則ったリストUIのシンプルな子ビューを作るのに便利なListTileについて記事をかきました。
2021.01.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Flutter は動画コンテンツも充実していて週毎にウィジェットを紹介している Flutter Widget of the Week では短い動画でウィジェットの概要を掴むことができる動画を視聴できます。

Flutter Widget of the Week を見るだけでも勉強になるのですが、記憶力が良くないので、視聴によるインプットだけでなく手元でコードを動かしてそれをブログ記事にする所までやって後から思い出しやすいようにしてみるシリーズです。

これまでの記事は以下のリンクから参照できます。

今回取り上げるのは ListTile です。

ListTile

リスト構造のUIを表現したい時にTileやContainerを使って好きにUIを表現することも多いですが、ListTileを使用することでコンストラクタの各パラメータに値を渡していくだけでマテリアルデザインに則ったリストUIの要素を表現できます。

動画での解説は以下になります。

ListTile内部のパーツはオプションも含めれば4つです。

  • 先頭のWidget
  • タイトルを表すWidget
  • サブタイトルを表すWidget
  • 末尾のWidget

それぞれコンストラクタ引数のleading、title、subtitle、trailingになります。

A single fixed-height row that typically contains some text as well as a leading or trailing icon.

とドキュメントに記載がある通り、柔軟に高さを変わることが想定されるUIではなくマテリアルデザインに則っていてかつ高さが変わらないシンプルなUIを表現するのに使うのが良さそうです。

引数の4つのWidgetすべてに値を渡してみます。

class MyApp extends HookWidget {
  // final privider = useProvider(subListNotifier);
  @override
  Widget build(BuildContext context) {
    final state = useProvider(subListNotifier.state);
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: Text('Manage subscription'),
          ),
          body: ListView.separated(
            padding: EdgeInsets.all(5),
            itemBuilder: (BuildContext context, int index) {
              var sub = state[index];
              return SubListItem(
                title: sub.name,
                subTitle: '¥${sub.cost}',
                tileColor: sub.color,
                leading: ConstrainedBox(
                    constraints: BoxConstraints(
                        minHeight: 44,
                        minWidth: 34,
                        maxHeight: 64,
                        maxWidth: 54),
                    child: FlutterLogo()),
              );
            },
            separatorBuilder: (BuildContext context, int index) {
              return SizedBox(height: 10);
            },
            itemCount: state.length,
          ),
        ));
  }
}

class SubListItem extends StatelessWidget {
  final String title;
  final String subTitle;
  final Widget leading;
  final Color tileColor;

  SubListItem({this.title, this.subTitle, this.leading, this.tileColor});

  @override
  Widget build(BuildContext context) {
    return ListTile(
        title: Text(title),
        subtitle: Text(subTitle),
        leading: leading,
        onTap: () => {},
        onLongPress: () => {},
        trailing: Icon(Icons.more_vert),
    );
  }
}

上記のコードはListView.itemBuilderを使って動的にリストUIを生成しています。SubListItemクラスにListの子ビューを切り出していて、その内部でListTileを使っています

実行結果は以下です。

初見で何のことかわからなかったのですが、引数denseは垂直方向に密集したリストの一部かどうかを表しています。trueだとheightとdefaultTextStyleのサイズが縮小されます。画像を見ると違いがわかるかと思います。変更箇所はdense: trueのみです。

引数tileColorを指定するとtap時、longPress時にエフェクトがかかっていないように見える

ListTile(
    title: Text(title),
    subtitle: Text(subTitle),
    leading: leading,
    onTap: () => {},
    onLongPress: () => {},
    trailing: Icon(Icons.more_vert),
    tileColor: tileColor // 任意の値
);

上記のように引数tileColorにColorを指定するとタップ時などにエフェクトがかからずタップしたかどうかわからなくなりました。今回はエフェクトの色などを考えたくなかったので、Ink Widgetでラップしてエフェクトが効くようにしました。

Ink(
    color: tileColor,
    child: ListTile(
    title: Text(title),
    subtitle: Text(subTitle),
    leading: leading,
    onTap: () => {},
    onLongPress: () => {},
    trailing: Icon(Icons.more_vert),
    dense: true,
  ),
);

以上のコードでエフェクトが効くようになりました。

ListTileのカスタマイズ

ListTileThemeで、このウィジェットのサブツリーの ListTile の色とスタイルのパラメータを定義することができます。ここで指定された値は、明示的な非 null 値が与えられていない ListTile プロパティに使用されます。

ListTileTheme(
    textColor: Colors.white,
    child: ListTile(
        title: Text(title),
        subtitle: Text(subTitle),
        leading: leading,
        onTap: () => {},
        onLongPress: () => {},
        trailing: Icon(Icons.more_vert),
        dense: true,
    ),
)

結果は以下のようになります。

ListTileThemeでは指定できないタップ時のエフェクトの色などを変更したい場合は、Themeウィジェットでラップするとカスタマイズできます。

アプリ全体のThemeDataを変更してしまうと、すべてのウィジェットに影響が出てしまうので基本的にListTileのみに適用したいスタイルの場合はThemeでラップするのが良さそうです。

Theme(
    data: ThemeData(
    splashColor: Colors.red,
    highlightColor: Colors.black.withOpacity(.5),
   ),
   child: ListTile(
     title: Text(title),
     subtitle: Text(subTitle),
     leading: leading,
     onTap: () => {},
     onLongPress: () => {},
     trailing: Icon(Icons.more_vert),
     dense: true,
),

タップ時、ロングプレス時のエフェクトの色が変わっているのが確認できます。

ListTileをDrawerに使用する

ListTileThemeのstyle引数にわたすことのできるListTileStyleという列挙型にdrawerというケースがあります。ListTileはDrawerの部品の一部として使用できることがわかります。

ListTileThemeを使うのも悪くないですが、ListTileが想定しているユースケースでない大抵の場合はCardやContainerで自由にレイアウトを組むことが多そうに感じています。

それとは別にDrawerのリストは単純なものが多いのでListTileが使えそうなケースも多いように感じています。

簡単にコードを書いてみました。

<br />import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: SamplePage(),
    ),
  );
}

class SamplePage extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Drawer'),
      ),
      endDrawer: Drawer(
        child: ListView(
          children: <Widget>[
            DrawerHeader(
              child: Text('ヘッダー'),
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
            ListTile(
              title: Text("ボタン"),
              trailing: Icon(Icons.arrow_forward),
            ),
          ],
        ),
      ),
    );
  }
}

実行結果は以下のようになります。

Drawerについては過去に記事を書いています。

FlutterのWidgetをコードを動かしながら学ぶ: Drawer編 | Developers.IO

まとめ

作りたいアプリが最近思い浮かんだのでFlutterを採用しました。Flutterの学習・開発は始めたばかりで説明やその裏にある認識に誤りがあるかもしれません。なにかお気づきの際はコメントなどでお知らせください。

参考にしたリンク一覧

記事中で紹介したリンク以外に参考にした記事があるので列挙します。

CardとListTileっぽいWidgetの書き方|Shinichiro Iwatsuru|note

A complete guide to Flutter’s ListTile | by Suragch | Medium

listview - Flutterで選択したときにListTileの背景色を変更する

How to Change ListTile Background Color On Selection ? - Flutter Agency