はじめに
Flutterで、1つの画面内に複数のListViewやGridViewを並べたい時があったので、その際の記録を記述しています。
(GridViewを使った実装コード例はありません)
まとめ
Sliver使おう。
シンプルにListViewをネストし並べる
実装概要
簡単に実装して見るとしたら、ListViewの中にさらに複数のListView (ListView.physics
にNeverScrollableScrollPhysicsを指定しておく) という実装かなあ、と思います。
ListView(
shrinkWrap: true,
children: <Widget>[
_ScrollView(label: '1st', baseColor: Colors.blue),
_ScrollView(label: '2nd', baseColor: Colors.yellow),
_ScrollView(label: '2nd', baseColor: Colors.yellow),
...
],
);
//_ScrollViewの中身
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: (_, int index) {
final String label = '${widget.label}_item_$index';
return _ListItem(..);
},
)
親のListViewの代わりにSingleChildScrollViewとColumnの組み合わせで実装される方もいらっしゃると思います。
実装(コード)
サンプルコード全文は以下に置いときます。
コードサンプル
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
shrinkWrap: true,
children: <Widget>[
_ScrollView(
key: const ValueKey<String>('ListView.children#1'),
label: 'ListView.children#1',
baseColor: Colors.blue,
),
_ScrollView(
key: const ValueKey<String>('ListView.children#2'),
label: 'ListView.children#2',
baseColor: Colors.yellow,
),
_ScrollView(
key: const ValueKey<String>('ListView.children#3'),
label: 'ListView.children#3',
baseColor: Colors.red,
),
_ScrollView(
key: const ValueKey<String>('ListView.children#4'),
label: 'ListView.children#4',
baseColor: Colors.orange,
),
_ScrollView(
key: const ValueKey<String>('ListView.children#5'),
label: 'ListView.children#5',
baseColor: Colors.green,
),
],
);
}
}
class _ScrollView extends StatefulWidget {
const _ScrollView({
Key key,
this.label,
this.baseColor,
}) : super(key: key);
final String label;
final Color baseColor;
@override
__ScrollViewState createState() => __ScrollViewState();
}
class __ScrollViewState extends State<_ScrollView> {
final itemCount = 50;
@override
Widget build(BuildContext context) {
print('$runtimeType (${widget.key}) build');
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: (_, int index) {
final String label = '${widget.label}_item_$index';
return _ListItem(
key: ValueKey<String>(label),
child: Container(
constraints: const BoxConstraints.expand(height: 48),
color: widget.baseColor.withOpacity(1 / (index % 5 + 1)),
child: Center(child: Text(label)),
),
);
},
);
}
}
class _ListItem extends StatelessWidget {
const _ListItem({Key key, this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
print('$runtimeType ($key) build');
return child;
}
}
この実装での問題
(1)パフォーマンスがあまりよくないです。
(2)スクロールをして _ScrollView から次に続く _ScrollView が表示されようとした時、一瞬スクロールが引っかかります
(3)最上位のListView.childrenに指定した _ScrollView (実体はほぼListView) がビルドされたと同時に、 _ScrollView の中のListView.childrenのWidgetがビルドされてしまいます。
(2)については(3)のように「_ScrollViewがビルドされたと同時に_ScrollViewのListView.childrenのWidgetがビルド」されてしまうのが要因と思います。
特に(3)についてですが、ListViewの子要素はlazilyに作られるはずなのに、その恩恵を受けられていません。
また_ScrollViewのListView.childrenのWidgetのうち、画面外に移動したものでも、それらが破棄されることはありません。
どうするか
CustomScrollViewとSliverを使います。
ListViewに以下のようにあります。
Transitioning to CustomScrollView
A ListView is basically a CustomScrollView with a single SliverList in its CustomScrollView.slivers property.
If ListView is no longer sufficient, for example because the scroll view is to have both a list and a grid, or because the list is to be combined with a SliverAppBar, etc, it is straight-forward to port code from using ListView to using CustomScrollView directly.
これを読んでCustomScrollViewを使う方が良さそうだと思いました。(具体的な根拠なし)
ただCustomScrollView.sliver
にはRenderSliverを生成するWidgetじゃなければダメとCustomScrollViewのドキュメントにあるので、
Widgets in these slivers must produce RenderSliver objects.
単にListViewをCustomScrollViewにするだけでは動かなそうです。
CustomScrollView + Sliverで作り直した例
実装修正・説明
修正後のコードはこの後の 「実装(Sliverを使用したコード)」 をご確認ください。
まず最上位のListViewをCustomScrollViewに置き換えます。
class BodyWithSliver extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
slivers: <Widget>[
...
],
);
}
_ScrollViewはどうしよう。。
とりあえずSliverListの継承クラスにしてやりきろうと思います。
SliverList.delegate
にはSliverChildBuilderDelegateを使用しました。
class _ScrollViewSliver extends SliverList {
_ScrollViewSliver({
Key key,
String label,
Color baseColor,
@required int itemCount,
}) : assert(itemCount != null && itemCount > 0),
super(
key: key,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final String _label = '${label}_item_$index';
return _ListItem(
key: ValueKey<String>(_label),
child: Container(
constraints: const BoxConstraints.expand(height: 48),
color: baseColor.withOpacity(1 / (index % 5 + 1)),
child: Center(child: Text(_label)),
),
);
},
childCount: itemCount,
addSemanticIndexes: false,
),
);
}
SliverChildListDelegateでも同じように作れました。
なお、SliverChildBuilderDelegateのドキュメントには
- IndexedSemanticsでAnnotateすることが必要
- 以下の内容より、1つのscroll view (多分CustomScrollViewのことだと思うが)に複数のdelagateがある場合はindexが正しくない
If multiple delegates are used in a single scroll view, then the indexes will not be correct by default.
という内容もありましたが、以下の理由から addSemanticIndexes: false
としています。
- IndexedSemanticsでAnnotateしてもらわなくても表示やスクロールには問題なさそう
- IndexedSemanticsでAnnotateしてもらうとTalkOver/Voiceoverでスクロール位置に応じたお知らせをしてくれる(?)、という機能があるようですが、それは今は必要なし
これでほぼ実装完了で、ログを確認したところ、以下が解決できていました。
(2)スクロールをして_ScrollViewから次に続く_ScrollViewが表示され長とした時、一瞬スクロールが引っかかります
(3)最上位のListView.childrenに指定した _ScrollView (実体はほぼListView) がビルドされたと同時に、_ScrollViewのListView.childrenのWidgetがビルドされてしまいます。
パフォーマンスはどうなんだろう・・・いっぺんにビルドがされない(lazilyなビルドをする)ので、パフォーマンスは良くなっている、と思ってます。。
実装(Sliverを使用したコード)
サンプルコード全文は以下に置いときます。
コードサンプル
class BodyWithSliver extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
slivers: <Widget>[
_ScrollViewSliver2(
key: const ValueKey<String>('ListView.children#1'),
label: 'ListView.children#1',
baseColor: Colors.blue,
itemCount: 50,
),
_ScrollViewSliver(
key: const ValueKey<String>('ListView.children#2'),
label: 'ListView.children#2',
baseColor: Colors.yellow,
itemCount: 50,
),
_ScrollViewSliver2(
key: const ValueKey<String>('ListView.children#3'),
label: 'ListView.children#3',
baseColor: Colors.red,
itemCount: 50,
),
_ScrollViewSliver(
key: const ValueKey<String>('ListView.children#4'),
label: 'ListView.children#4',
baseColor: Colors.orange,
itemCount: 50,
),
_ScrollViewSliver2(
key: const ValueKey<String>('ListView.children#5'),
label: 'ListView.children#5',
baseColor: Colors.green,
itemCount: 50,
),
],
);
}
}
/// SliverChildBuilderDelegateを使った例
class _ScrollViewSliver extends SliverList {
_ScrollViewSliver({
Key key,
String label,
Color baseColor,
@required int itemCount,
}) : assert(itemCount != null && itemCount > 0),
super(
key: key,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final String _label = '${label}_item_$index';
return _ListItem(
key: ValueKey<String>(_label),
child: Container(
constraints: const BoxConstraints.expand(height: 48),
color: baseColor.withOpacity(1 / (index % 5 + 1)),
child: Center(child: Text(_label)),
),
);
},
childCount: itemCount,
addSemanticIndexes: false,
),
);
}
/// SliverChildListDelegateを使った例
class _ScrollViewSliver2 extends SliverList {
_ScrollViewSliver2({
Key key,
String label,
Color baseColor,
@required int itemCount,
}) : assert(itemCount != null && itemCount > 0),
super(
key: key,
delegate: SliverChildListDelegate(
List<Widget>.generate(itemCount, (int index) {
final String _label = '${label}_item_$index';
return _ListItem(
key: ValueKey<String>(_label),
child: Container(
constraints: const BoxConstraints.expand(height: 48),
color: baseColor.withOpacity(1 / (index % 5 + 1)),
child: Center(child: Text(_label)),
),
);
}),
addSemanticIndexes: false,
),
);
}
class _ListItem extends StatelessWidget {
const _ListItem({Key key, this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
print('$runtimeType ($key) build');
return child;
}
}