【Flutter】複数のリストをならべる方法(Column, ListView, Sliver)
何をしたかったか
異なるデータを参照するリストを同じ画面に複数表示したい
例えば以下のようなレイアウトを作成する場合
お知らせ一覧
|- お知らせ 1
|- お知らせ 2
|- お知らせ 3 (あるだけ全部)
お気に入り一覧
|- お気に入り 1
|- お気に入り 2
|- お気に入り 3
|- お気に入り 4 (あるだけ全部)
履歴
|- 履歴 1
|- 履歴 2
|- 履歴 3 (あるだけ全部)
(あるだけ全部表示するのはレイアウト的に微妙な気がしますが例として😇)
実装方法
1. Columnで実装
final list1 = List.generate(
30, (int i) => TestTile(listIndex: 0, tileIndex: i),
);
final list2 = List.generate(
30, (int i) => TestTile(listIndex: 1, tileIndex: i),
);
return Scaffold(
appBar: AppBar(),
body: SingleChildScrollView(
child: Column(
children: [
const Text('List1'),
...list1,
const Text('List2'),
...list2,
],
),
),
);
'Column'内に直接Widgetの配列として、リストを追加する。
以下2つの方法に比べシンプルな方法です
body: SingleChildScrollView(
child: Column(
children: [
bodyの子に'Column'を設定するだけだと、画面外にリストがはみ出してしまうためレイアウトエラーが発生&スクロールができないため'SingleChildScrollView'を親に設定しています。
2. ListViewで実装
final list1 = List.generate(
30, (int i) => TestTile(listIndex: 0, tileIndex: i),
);
final list2 = List.generate(
30, (int i) => TestTile(listIndex: 1, tileIndex: i),
);
final listView1 = ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, i) => i == 0 ? const Text('List1') : list1[i - 1],
itemCount: list1.length + 1,
);
final listView2 = ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, i) => i == 0 ? const Text('List2') : list2[i - 1],
itemCount: list2.length + 1,
);
final listItems = [
listView1,
listView2,
];
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: ((_, i) => listItems[i]),
itemCount: listItems.length,
),
);
複数のListView
でリストを表示します。
ListView
は用意されている領域すべてを埋めるような特徴があるので、ListView
(Column
も)の子にListView
を配置すると、高さの上限設定が存在しないため無限に高さを広げようとしてレイアウトエラーになってしまいます。
そのため、以下の設定が必要になります。
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
- shrinkWrap : trueにすることで、ListViewの高さをListView内で表示している要素をすべて表示したときの高さになります。(
ListView.height
=item*itemCount
) - physics : デフォルトの設定だと、ListViewは自身のView内をスクロールすると、自身の要素がスクロールするようになっています。そのため、ListView内にListViewを配置すると親のListViewのスクロール処理と、子のListViewのスクロール処理が重なってしまい、正常にスクロールできなくなってしまいます。そのため、子のListViewについては、
NeverScrollableScrollPhysics
を設定しスクロールをできないように設定する必要があります。
3. Sliverで実装
final list1 = List.generate(
30, (int i) => TestTile(listIndex: 0, tileIndex: i),
);
final list2 = List.generate(
30, (int i) => TestTile(listIndex: 1, tileIndex: i),
);
return Scaffold(
appBar: AppBar(),
body: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate([
const Text('List1'),
...list1,
]),
),
SliverList(
delegate: SliverChildListDelegate([
const Text('List2'),
...list2,
]),
),
],
),
);
Sliver
というクラス群を利用して、リストを表示します。
CustomScrollView
というクラスにあるslivers
というパラメーターに要素を追加していきます。
今回はListView
のような見た目を再現したかったのでSliverList
を利用しています。
注意点
上記3つの方法で要素数に上限がなく、表示するWidget上に表示する内容が多くメモリに負荷がかかる可能性がある場合、1,2の方法については避けるべきです。
逆に表示する要素が決まっており、少数でメモリ的にもあまり圧迫しないのであれば利用することは全然ありかと思います。
Columnにはそもそもメモリの管理機能はなく、子要素に設定されたすべての要素を常に表示されています。
ListView.builder
には画面に表示されなくなった要素は一度破棄され必要以上にメモリが圧縮されない仕組みが存在しています。
ただ、2の方法ではListView内にListViewを配置するためにshrinkWrap
というパラメータをtrueにしている影響ですべての要素を表示しているためColumn同様、表示されない要素がメモリ上破棄されることはなくメモリを圧迫してしまいます。
検証用のWidget
initState
でWidgetが生成されるタイミングで呼ばれ、
dispose
はWidgetが破棄されるタイミングでよばれます。
ListView.builder
やSliver
を利用する場合、基本的には表示されなくなった場合にdispose
が呼ばれ、表示されるタイミングでinitState
が呼ばれます。
class TestTile extends StatefulWidget {
const TestTile({super.key, required this.listIndex, required this.tileIndex});
final int listIndex;
final int tileIndex;
@override
State<StatefulWidget> createState() => TestTileState();
}
class TestTileState extends State<TestTile> {
@override
void initState() {
super.initState();
print('initState List:${widget.listIndex} : Tile:${widget.tileIndex}');
}
@override
void dispose() {
super.dispose();
print('dispose List:${widget.listIndex} : Tile:${widget.tileIndex}');
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text('List:${widget.listIndex} : Tile:${widget.tileIndex}'),
);
}
}
以下、画面遷移直後のTerminalに表示されたログ
1. Columnでの実装
の場合は、list1
とlist2
の内容がすべて表示されているとみなされ、すべての要素のinitStateが呼ばれています。
2. ListViewで実装
での実装の場合は、listView1
の要素がすべて表示されているとみなされます。下にスクロールをしてlistView2
が表示されようとすると、listView2
の要素が一斉に表示されます。
3. Sliverで実装
での実装の場合は、list1
の表示されている部分(@buffer)の要素のみ表示されているとみなされ、必要最低限のinitState
が呼ばれます。
上記で記載しているコードは以下リポジトリにアップしています。
Discussion