😎

【Flutter Widget基礎】ListView

2024/08/18に公開

基本的な利用方法

ListViewからは4通りの生成方法を提供しています。

  • デフォルト生成
  • ListView.builder
  • ListView.separated
  • ListView.custom

具体的な例を通してそれぞれの利用方法を見ていきましょう。

1. デフォルト生成

子widgetを順番に並ぶ

ListView(
        padding: const EdgeInsets.all(10.0),
        children: [
          Container(
            height: 50,
            color: Colors.red,
            child: const Center(child: Text('赤色')),
          ),
          Container(
            height: 50,
            color: Colors.yellow,
            child: const Center(child: Text('黄色')),
          ),
          Container(
            height: 50,
            color: Colors.green,
            child: const Center(child: Text('緑色')),
          ),
          Container(
            height: 50,
            color: Colors.blue,
            child: const Center(child: Text('青色')),
          ),
        ],
      )

2. ListView.builder

builderメソッドを使用して、ListやMapデータからリストを一括生成する

const colorMap = {
      "赤色":Colors.red,
      "黄色":Colors.yellow,
      "緑色":Colors.green,
      "青色": Colors.blue,
    };

ListView.builder(
        padding: const EdgeInsets.all(10.0),
        itemCount: colorMap.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            height: 50,
            color: colorMap.values.toList()[index],
            child: Center(
                child: Text(colorMap.keys.toList()[index])),
          );
        })

3. ListView.separated

separatedメソッドはbuilderメソッドに似ていますが、separatorBuilder属性を使用して区切り線を作成することができます。

const colorMap = {
      "赤色":Colors.red,
      "黄色":Colors.yellow,
      "緑色":Colors.green,
      "青色": Colors.blue,
    };

ListView.separated(
        padding: const EdgeInsets.all(10.0),
        itemCount: colorMap.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            height: 50,
            color: colorMap.values.toList()[index],
            child: Center(
                child: Text(colorMap.keys.toList()[index])),
          );
        },
        separatorBuilder: (BuildContext context, int index) {
          return Container();
        },
      )

4. ListView.custom

customメソッドはより高度なカスタマイズが可能なリストを生成できます。通常の要件ではbuilderメソッドとseparatedメソッドが十分なので、あまり利用する機会がないかもしれません。

const colorMap = {
      "赤色":Colors.red,
      "黄色":Colors.yellow,
      "緑色":Colors.green,
      "青色": Colors.blue,
    };
ListView.custom(
          childrenDelegate: SliverChildBuilderDelegate(
              childCount: colorMap.length,
              (BuildContext context, int index) {
                return Container(
                  height: 50,
                  color: colorMap.values.toList()[index],
                  child: Center(
                      child: Text(colorMap.keys.toList()[index])),
                );
          })
      )

複雑なリストを作成する

データクラスを作成

class TodoItem {
  ImageProvider image;
  String title;
  String description;
  String category;
  String startDate;
  ImageProvider assignee;
  TodoItem({required this.image,
    required this.title,
    required this.description,
    required this.category,
    required this.startDate,
    required this.assignee
  });
}

ListItemを作成する

typedef OnItemClickListener = void Function();
class TodoItemView extends StatelessWidget {

  final TodoItem data;
  final OnItemClickListener onItemClickListener;

  const TodoItemView({super.key, required this.data, required this.onItemClickListener});

  
  Widget build(BuildContext context) {

    var todoImage = Container(
        decoration: BoxDecoration(
          color: Colors.white,
          shape: BoxShape.rectangle,
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.3),
              offset: const Offset(0.0, 0.0),
              blurRadius: 3.0,
              spreadRadius: 0.0,
            ),
          ],
        ),
        width: 70,
        height: 70,
        child: Padding(
          padding: const EdgeInsets.all(3),
          child: Image(image: data.image, fit: BoxFit.fill)),
        );

    var center = Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(data.title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
        Padding(
          padding: const EdgeInsets.only(top: 0, bottom: 8),
          child: Text(data.description, style: const TextStyle(color: Colors.grey, fontSize: 12)),
        ),
      ],
    );

    var foot =  Wrap(
        crossAxisAlignment: WrapCrossAlignment.center,
        alignment: WrapAlignment.center,
        spacing: 4,
        children: [
          const Icon(Icons.calendar_today_outlined, color: Colors.blue, size: 12),
          Text(data.startDate, style: const TextStyle(fontSize: 12, height: 1, color: Color(0xff86909c))),
          const SizedBox(width: 3),
          const Icon(Icons.category,size: 12),
          Text(data.category, style: const TextStyle(fontSize: 12, height: 1, color: Color(0xff86909c))),
        ],
      );

    //assignee
    var assigneeImage = Container(
      decoration: BoxDecoration(
        color: Colors.white,
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.3),
            offset: const Offset(0.0, 0.0),
            blurRadius: 3.0,
            spreadRadius: 0.0,
          ),
        ],
      ),
      width: 40,
      height: 40,
      child: Padding(
          padding: const EdgeInsets.all(0),
          child: Image(image: data.assignee, fit: BoxFit.scaleDown)),
    );

    var item = Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        const SizedBox(width: 10),
        todoImage,
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              center,
              foot
            ],
          ),
        ),
        Expanded(
          child: assigneeImage,
        ),
        const SizedBox(width: 10),
      ],
    );
    var result = Card(
        elevation: 5,
        child: InkWell(
            onTap: onItemClickListener,
            child: Padding(
              padding: const EdgeInsets.all(5),
              child: item,
            )));
    return result;
  }
}

ListViewを生成する

ダミーデータを利用してリストを作成します。
今回はbuilderメソッドを利用して生成していますが、区切り線を追加したい場合はseparatedメソッドをご利用ください

    //ダミーデータを用意する
    var data = <TodoItem>[];
    for (var i = 0; i < 20; i++) {
      data.add(TodoItem(
        image: const AssetImage("images/read_book.jpg"),
        title: "$i:読書する",
        description: "自由研究の本を読む",
        startDate: "2024-09-01",
        category: '未分類',
        assignee: const AssetImage("images/profile.png"),
      ));
    }

    ListView.builder(
          padding: EdgeInsets.all(8.0),
          itemCount: data.length, 
          itemBuilder: (BuildContext context, int index) {
            return TodoItemView(
              data: data[index],
              onItemClickListener: () {
                print("item $index clicked");
              },
            );
        })

異なるスタイルをListItemに適用する

よくある場面としてLINEのチャット画面が思い出せるでしょう。今回はそのチャット画面を例として実現方法を解説します。

Chat Messageをスタイル別で生成するコード例となります。

enum ChatType { right, left }

class ChatItem {
  ImageProvider icon;
  ChatType type;
  String message;

  ChatItem({required this.icon,
    required this.type,
    required this.message,
  });
}

class ChatWidget extends StatelessWidget {
  final ChatItem chatItem;

  const ChatWidget({super.key, required this.chatItem});

  
  Widget build(BuildContext context) {

    var icon = Container(
      decoration: BoxDecoration(
        color: Colors.white,
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.3),
            offset: const Offset(0.0, 0.0),
            blurRadius: 3.0,
            spreadRadius: 0.0,
          ),
        ],
      ),
      width: 40,
      height: 40,
      child: Padding(
          padding: const EdgeInsets.all(0),
          child: Image(image: chatItem.icon, fit: BoxFit.scaleDown)),
    );

    var leftItem =  Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        const SizedBox(width: 10),
        icon,
        Padding(
            padding: const EdgeInsets.symmetric(horizontal: 10),
            child: Container(
              padding: const EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: Colors.yellow,
                borderRadius: BorderRadius.circular(10),
              ),
              width: 250,
              child: Text(chatItem.message),
            )
        ),
      ],
    );

    var rightItem = Padding(
        padding: const EdgeInsets.only(top: 10, bottom: 10),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            const SizedBox(width: 10),
            Padding(
                padding: const EdgeInsets.symmetric(horizontal: 10),
                child: Container(
                  padding: const EdgeInsets.all(10),
                  decoration: BoxDecoration(
                    color: Colors.blue,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  width: 250,
                  child: Text(chatItem.message),
                )
            ),
            icon
          ],
        )
    );

    switch (chatItem.type) {
      case ChatType.right:
        return rightItem;
      case ChatType.left:
        return leftItem;
    }
  }
}

ダミーデータを利用してListViewを生成するコード例です。

    var random = Random();
    var chatData = <ChatItem>[];
    var chatStrs = [  "新橋で新しい焼肉屋ができたらしい。飲みに行こうか?太郎くんも呼んであげて。",  "焼肉は俺の大好物だからぜひ週末食べにいきたい",  "今週末で台風がくるらしい、家でのんびり過ごしたほうが良いよ。焼肉屋へいくのは来週にしましょう"];
    for (var i = 0; i < 20; i++) {
      chatData.add(ChatItem(
          icon: const AssetImage("images/profile.png"),
          message: chatStrs[random.nextInt(chatStrs.length)],
          type: i.isEven ? ChatType.left : ChatType.right));
    }

    ListView.builder(
          padding: EdgeInsets.all(8.0),
          itemCount: data.length, 
          itemBuilder: (BuildContext context, int index) {
            return ChatWidget(
              chatItem: chatData[index],
            );
          })

Discussion