本記事は下記リンクのFlutter公式ドキュメントを学習したメモです。

Layout in Flutter

今回は上記リンクのレイアウトの中でList & gridsセクションの内容を紹介する。

リストビュー

基本的なリストの表示。ListViewを使ってListTileを配置する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const title = '基本的なリスト';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: ListView(
          children: const <Widget>[
            ListTile(
              // ラベルの前に位置するアイコン
              leading: Icon(Icons.map),
              // リストタイルのラベル
              title: Text('Map'),
            ),
            ListTile(
              // ラベルの前に位置するアイコン
              leading: Icon(Icons.photo_album),
              // リストタイルのラベル
              title: Text('Album'),
            ),
            ListTile(
              // ラベルの前に位置するアイコン
              leading: Icon(Icons.call),
              // リストタイルのラベル
              title: Text('Phone'),
            ),
          ],
        ),
      ),
    );
  }
}

iOS simulator

横並びリスト

先ほどは基本的に縦並びのリストだったが、横並びもある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const title = '基本的なリスト';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        // 隙間とサイズ指定のためContainerを使う
        body: Container(
          // 内側の上下で隙間を作る
          margin: const EdgeInsets.symmetric(vertical: 20),
          // 縦のサイズを指定する
          height: 200,
          child: ListView(
            // 横向きにする(本当は横スクロールにする)
            scrollDirection: Axis.horizontal,
            children: <Widget>[
              // 横幅160ピクセルの箱を並べる
              Container(
                width: 160,
                color: Colors.red,
              ),
              Container(
                width: 160,
                color: Colors.blue,
              ),
              Container(
                width: 160,
                color: Colors.green,
              ),
              Container(
                width: 160,
                color: Colors.yellow,
              ),
              Container(
                width: 160,
                color: Colors.orange,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

iOS simulator

グリッド

グリッド表示したい場合はGridViewがある。GridView.count()コンストラクターを使うと行数もしくは列数を指定して初期化できる。

次のサンプルはListを使って100個のインデックスを作ってそのインデックスを持つ100個のアイテムを追加する例だ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const title = 'グリッドリスト';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        // 隙間とサイズ指定のためContainerを使う
        body: GridView.count(
          // 2列のグリッド
          crossAxisCount: 2,
          // 100個のアイテムを作る
          children: List.generate(100, (index) {
            // インデックスによって3つの色のどれかにする
            Color? bgColor = Colors.blue[100];
            if (index % 3 == 0) {
              bgColor = Colors.red[100];
            } else if (index % 3 == 1) {
              bgColor = Colors.green[100];
            }
            return Container(
              color: bgColor,
              child: Center(
                child: Text(
                  'アイテム${index + 1}番',
                  style: const TextStyle(
                    color: Colors.black,
                    fontSize: 14,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}

iOS simulator

異なるタイプを持つリスト

様々なタイプのアイテムを持つリストを作ってみる。

まずは異なるタイプをWidgetをリストに入れられるためにインターフェースを作る。

ListItemは主タイトルとサブタイトルのWidgetを作成する関数を持つインターフェースだ。

1
2
3
4
5
// ListItem Interface
abstract class ListItem {
  Widget buildTitle(BuildContext context);
  Widget buildSubtitle(BuildContext context);
}

次に上で作ったインターフェースを使って二つの異なるWidgetを作る。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class HeadingItem implements ListItem {
  final String heading;

  HeadingItem(this.heading);

  @override
  Widget buildTitle(BuildContext context) {
    return Text(
      heading,
      style: Theme.of(context).textTheme.headlineSmall,
    );
  }

  @override
  Widget buildSubtitle(BuildContext context) => const SizedBox.shrink();
}

class MessageItem implements ListItem {
  final String sender;
  final String body;

  MessageItem(this.sender, this.body);

  @override
  Widget buildTitle(BuildContext context) => Text(sender);

  @override
  Widget buildSubtitle(BuildContext context) => Text(body);
}

HeadingItemののサブタイトルはSizedBox().shrink()なのでほぼサイズがないアイテムだ。結局、タイトルのみ表示されるWidgetになる。

MessageItemは送信者とメッセージボディをもらい二つのテキストを生成している。

次にmain関数でデータを1000個作って渡したら、データに合わせてリストに追加するようにしてみよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import 'package:flutter/material.dart';

void main() {
  runApp(
    MyApp(
      items: List<ListItem>.generate(
        1000, // 1000個のデータを作成
        // 6個ごとにタイトルにする
        (i) => i % 6 == 0
            ? HeadingItem('タイトル $i')
            : MessageItem('送信者 $i', 'メッセージ $i'),
      ),
    ),
  );
}

class MyApp extends StatelessWidget {
  // 表示するデータ
  final List<ListItem> items;

  const MyApp({super.key, required this.items});

  @override
  Widget build(BuildContext context) {
    const title = 'いろんなタイプを持つリスト';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        // itemsリストからアイテムを作成する
        body: ListView.builder(
          // リストのサイズ
          itemCount: items.length,
          // リストの各要素からListTileを作る
          itemBuilder: (context, index) {
            final item = items[index];
            return ListTile(
              title: item.buildTitle(context),
              subtitle: item.buildSubtitle(context),
            );
          },
        ),
      ),
    );
  }
}

iOS simulator

iOSシミュレーターで実行すると「Using the Impeller rendering backend.」とエラーが発生するが一旦無視しよう。おそらくデータが多すぎたと思う。

柔軟なリスト

タイトルが変化もしれないが、通常の画面では要素を均等に配置して画面が小さい時にはスクロールできるように配置するものを作ってみる。

まずはアイテムとなるWidgetだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ItemWidget extends StatelessWidget {
  const ItemWidget({
    super.key,
    required this.text,
  });

  final String text;

  @override
  Widget build(BuildContext context) {
    // 縦幅200ピクセルのカード
    return Card(
      child: SizedBox(
        height: 200,
        child: Center(
          child: Text(text),
        ),
      ),
    );
  }
}

リストWidgetを含め、main関数まで

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import 'package:flutter/material.dart';

void main() => runApp(const SpacedItemList());

class SpacedItemList extends StatelessWidget {
  const SpacedItemList({super.key});

  @override
  Widget build(BuildContext context) {
    // 4つのアイテムを持つ
    const items = 4;
    return MaterialApp(
      title: 'Spaced Item List',
      // アプリで使うテーマ(色)を定義する
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        cardTheme: CardTheme(color: Colors.blue.shade50),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: LayoutBuilder(
          builder: (context, constraints) {
            return SingleChildScrollView(
              child: ConstrainedBox(
                constraints: BoxConstraints(minHeight: constraints.maxHeight),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: List.generate(
                    items,
                    (index) => ItemWidget(text: 'アイテム $index'),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

iOS simulator

カードのサイズを変えたり、アイテム数を変えたり試してみたら、画面を超えるとスクロールできるし、画面におさまる場合は均等な間隔で並ぶことがわかる。

長いリスト

ListViewコンストラクターはアイテム数が少ない時に良い。アイテム数が多くなるとListView.builderコンストラクターを使った方が良い。

ListViewは一度にアイテムを作成するが、ListView.builderはスクロールされる時に作成される。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import 'package:flutter/material.dart';

void main() {
  runApp(
    MyApp(
      items: List<String>.generate(10000, (index) => 'アイテム ${index + 1}'),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.items});

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    const title = '長いリスト';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: ListView.builder(
          itemCount: items.length,
          prototypeItem: ListTile(
            leading: const Icon(Icons.account_circle),
            title: Text(items.first),
          ),
          itemBuilder: (_, index) {
            return ListTile(
              leading: const Icon(Icons.account_circle),
              title: Text(items[index]),
            );
          },
        ),
      ),
    );
  }
}

iOS simulator

最後に

これでLayoutのList & gridsセクションの内容を一通りみた。

リストの表示でどのようにすれば良いか、わかったが、長いリストの場合、工夫が必要と思う。

コメントする