Flutter

Flutterでグループ単位のListViewを表示する方法

記事内に商品プロモーションを含む場合があります

grouped_list を使用する

Flutterを使って、グループ単位でListViewを表示するにはどうすればいいのかという壁にぶつかったのですが、「grouped_list」 というパッケージを作ってくださっている方がいました。

これを使うと非常に簡単に実装することができます。

grouped_listについて使用方法を解説します。

使用手順

pubspec.yamlに追記

dependencies:
  grouped_list: ^3.7.1

 

パッケージをimportする

import 'package:grouped_list/grouped_list.dart';

 

Widgetを作成する

まずはpub.devのサンプルコードを試してみます。

List<Map<String, String>> _elements = createElements();
return GroupedListView<dynamic, String>(
        elements: _elements,
        groupBy: (element) => element['key'],
        groupSeparatorBuilder: (String groupByValue) => Text(groupByValue),
        itemBuilder: (context, dynamic element) => Text(element['text']),
        itemComparator: (item1, item2) => item1['text'].compareTo(item2['text']), // optional
        useStickyGroupSeparators: true, // optional
        floatingHeader: true, // optional
        order: GroupedListOrder.ASC, // optional
      );

List<Map<String, String>> createElements() {
    List<Map<String, String>> res = List();
    Map<String, String> map1 = {'key': 'key1', 'text': 'text1-1'};
    Map<String, String> map2 = {'key': 'key2', 'text': 'text2-3'};
    Map<String, String> map3 = {'key': 'key2', 'text': 'text2-2'};
    Map<String, String> map4 = {'key': 'key2', 'text': 'text2-1'};
    Map<String, String> map5 = {'key': 'key3', 'text': 'text3-1'};
    res.add(map1);
    res.add(map2);
    res.add(map3);
    res.add(map4);
    res.add(map5);
    return res;
}

各引数の説明

引数 説明
elements List<T> 表示したいリストを設定します。Tにはグルーピングのキーを設定できる値を持たせておく必要があります。
groupBy E elementsに設定した値のTの一つが引数のFunctionです。渡されたTからグルーピングに使用する値を返却してください。
groupSeparatorBuilder Widget グルーピングごとのセパレーターを設定します。サンプルだとgroupByで返却している値をそのまま使用しています。
itemBuilder Widget 1つのデータごとの表示するWidgetを設定します。
itemComparator int グループ内のソート条件を設定します。設定は任意です。
useStickyGroupSeparators bool グルーピングごとのセパレーターを固定するかどうかの設定です。trueにした場合はスクロールしたときにセパレーターが固定されます。おそらく多くの人が求めているものはこれをtrueにした際の挙動です。
floatingHeader bool useStickyHeaderをtrueに設定している場合に、固定されたセパレーターの挙動を制御します。trueに設定すると、セパレーターが浮き上がりデータに被らなくります。falseにするとヘッダーの領域にデータが入ってくるとデータが見えなくなります。
order GroupedListOrder 昇順、降順を設定できます。

groupSeparatorBuilderとitemBuilderの設定値がキレイな画面を作るポイントですね。

私のサンプルだと文字を返却しているので見た目がかなりしょぼいです。

チャット画面を作成してみる

pub.devに画像だけサンプルで載っているチャット画面を再現してみました。

すごくいい感じ!

チャットの機能を導入しようと思ったらこんな感じで簡単に作れちゃいますね!

grouped_list おすすめです。

ソースはこんな感じです。

List<Map<String, String>> _elements = createElements();
return GroupedListView<dynamic, String>(
        elements: _elements,
        groupBy: (element) => element['key'],
        groupSeparatorBuilder: (String groupByValue) =>
            _createHeader(groupByValue),
        itemBuilder: (context, dynamic element) =>
            _createItem(context, element),
        itemComparator: (item1, item2) =>
            item1['time'].compareTo(item2['time']),
        useStickyGroupSeparators: true,
        floatingHeader: false,
        stickyHeaderBackgroundColor: Colors.white.withOpacity(0.5),
        order: GroupedListOrder.ASC, // optional
      );

List<Map<String, String>> createElements() {
    List<Map<String, String>> res = List();
    Map<String, String> map1 = {
      'key': '2020年12月2日',
      'text': '明日、夜ご飯食べに行かない?',
      'lr': 'l',
      'time': '18:12'
    };
    Map<String, String> map2 = {
      'key': '2020年12月3日',
      'text': 'すまん、返事遅れた!',
      'lr': 'r',
      'time': '09:24'
    };
    Map<String, String> map3 = {
      'key': '2020年12月3日',
      'text': 'いいね!行こうぜ!',
      'lr': 'r',
      'time': '09:26'
    };
    Map<String, String> map4 = {
      'key': '2020年12月3日',
      'text': 'じゃあ、19時に新橋駅前に集合で',
      'lr': 'l',
      'time': '09:35'
    };
    Map<String, String> map5 = {
      'key': '2020年12月5日',
      'text': '頭いてええええええ',
      'lr': 'r',
      'time': '14:12'
    };
    Map<String, String> map6 = {
      'key': '2020年12月5日',
      'text': '二日酔いじゃん。えっ昨日酒飲んでなくない',
      'lr': 'l',
      'time': '14:22'
    };
    Map<String, String> map7 = {
      'key': '2020年12月5日',
      'text': 'ぐわあああああああああああああああ',
      'lr': 'r',
      'time': '14:25'
    };
    Map<String, String> map8 = {
      'key': '2020年12月5日',
      'text': 'あかん、救急車呼ばな',
      'lr': 'l',
      'time': '14:29'
    };
    res.add(map1);
    res.add(map2);
    res.add(map3);
    res.add(map4);
    res.add(map5);
    res.add(map6);
    res.add(map7);
    res.add(map8);
    return res;
  }

  Widget _createHeader(String groupByValue) {
    return Padding(
      padding: const EdgeInsets.only(
          top: 8.0, bottom: 8.0, left: 120.0, right: 120.0),
      child: ClipRRect(
          borderRadius: BorderRadius.all(Radius.circular(12.0)),
          child: Container(
            color: Colors.lightBlue,
            child: Text(
              groupByValue,
              style:
                  TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
          )),
    );
  }

  Widget _createItem(BuildContext context, element) {
    return Padding(
      padding: element['lr'] == 'l'
        ? const EdgeInsets.only(top: 4.0, bottom: 4.0, right: 45.0, left: 8.0)
        : const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 45.0, right: 8.0),
      child: Card(
        elevation: 10.0,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
        color: Colors.white,
        //shadowColor: Colors.white24,
        child: ListTile(
          leading: element['lr'] == 'r'
              ? Padding(
                padding: const EdgeInsets.only(top: 4.0),
                child: Text(element['time'],),
              )
              : Icon(Icons.person_outline_rounded),
          title: Text(element['text']),
          trailing: element['lr'] == 'l'
            ? Text(element['time'])
            : Icon(Icons.person_rounded),
        ),
      ),
    );
  }