はじめに
Flutterで無限スクロールを実装する際のTipsというタイトルにしましたが、ScrollControllerを用いてリストを監視したい時に広く使えるTipsです。
ハマり所でもあると思いますので、本記事を書きました。
ListView / SliverList / SingleChildScrollView などで適用できます。
サンプル
https://github.com/HeavenOSK/monitoring_scroll
実装方法
実装する際のポイントは以下の2点です。
-
ListViewのScrollControllerに対して、現在の表示位置を監視するListnerを追加する - iOS風の「AppBar上部をタップすると、ListViewを初期位置まで戻す」操作を実現するために、
Scaffoldを提供するWidgetとListViewを提供するWidgetを分離し、親子構造にする
1について
現在の表示位置を監視して、下端に達した時に新たなコンテンツを追加する処理を行います。
2について
Flutterでは、iOS風の「AppBar上部をタップすると、ListViewを初期位置まで戻す」操作(以下,プライマリ操作)がサポートされています。
しかし、ScrollContollerでListViewを監視する処理を素直に実装すると、プライマリ操作が無効になります。
例)
https://stackoverflow.com/questions/46377779/flutter-implementing-a-infinitescroll-view
以下では、一旦iOS風のプライマリ操作のことを考慮せずに実装した例を載せたのち、なぜプライマリ操作が無効化されたかを踏まえた実装の例を載せます。
ListViewを監視する
実装順序は以下のようになります。
1. ScrollControllerを初期化する
2. ScrollControllerにListnerを追加して、更新処理を実装する
3. 作成したScrollControllerを、監視対象のリストのcontrollerプロパティに設定する
リストの監視はScrollControllerを介して行います。
ScrollControllerには、positionプロパティがあり、以下の値が取得できます。
-
position.maxScrollExtent=> ListView全体の下端位置 -
position.pixels=> 現在の表示位置
これらの値を比較して、「下端位置から20pixelの位置に達したら、コンテンツを読み込む」というような処理を追加します。
コードは以下のようになります。
StatefulWidgetのinitState()内で、ScrollControllerを初期化して、addListner()します。
ScrollController _scrollController;
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(() {
final maxScrollExtent = _scrollController.position.maxScrollExtent;
final currentPosition = _scrollController.position.pixels;
if (maxScrollExtent > 0 &&
(maxScrollExtent - 20.0) <= currentPosition) {
_addContents();
}
});
super.initState();
}
上記で作成した_scrollController をListViewのcontrollerプロパティに設定します。
ListView.builder(
controller: _scrollController,
itemBuilder: (context, index) {
return _buildListItem(_items[index]);
},
itemCount: _items.length,
),
ちなみに、コンテンツの追加処理は以下のようにしています。
今回はコードを単純にするためにsetState()で更新処理を行っています。
bool _isLoading = false;
_addContents() {
if (_isLoading) {
return;
}
_isLoading = true;
Future.delayed(Duration(seconds: 2), () {
setState(() {
Contents.forEach((content) => _items.add(content));
});
_isLoading = false;
});
}
ScrollControllerを用いてリストを監視する実装方法については以上です。
コード全体は以下に載せています。
https://github.com/HeavenOSK/monitoring_scroll/tree/008be42e2cb61352a0dda7ad71c0b74220fd0bdc/lib
この状態では、プライマリ操作は無効になってしまっています。
以下では、なぜプライマリ操作が無効になったのかを説明します。
なぜプライマリ操作が無効になったのか
Flutterのプライマリ操作は、Scaffold内で実装されています。
ScaffoldはappBar上部のTap操作を監視しており、appBar上部がTapされた際には内部の_primaryScrollControllerというScrollControllerを介してリストを初期位置に戻す操作を行います。
ScaffoldのbodyにてListViewを実装する際、ListViewのcontrollerプロパティやprimaryプロパティを指定しなければ、暗黙的にScaffold内の_primaryScrollControllerが適用されます。
つまり、上記のリストを監視する実装方法では、Scaffold内のprimaryScrollControllerではなく、独自に定義した_scrollControllerをListViewに適用しているため、プライマリ操作が無効になってしまいました。
以下では、プライマリ操作を有効にする実装方法を説明します。
ListViewのプライマリ操作を有効にする
上記の通り、プライマリ操作を有効にするには、ListViewにScaffold内の_primaryScrollControllerを適用する必要があります。
コードに以下の修正を加えます。
-
Scaffoldを提供するWidgetとListViewを提供するWidgetを分離して、親子構造にする -
ListViewを提供するWidget内にて、PrimaryScrollController.of(context)を用いてScaffold内の_primaryScrollControllerを呼び出し、addListner()を実装する
1.Scaffoldを提供するWidgetとListViewを提供するWidgetを分離して、親子構造にする
PrimaryScrollController.of(context)を使用するためには、呼び出し時に既にScaffoldがWidget Tree内に存在している必要があります。
Scaffoldを提供するWidgetとListViewを提供するWidgetを分離して、親子構造にすることで、ListViewの生成前にはScaffoldのbuild()が完了し、Widget Tree内に存在している状態を作ります。
(Widget Treeについてご興味がある方は、以下を参考にして下さい)
Flutter の Widget ツリーの裏側で起こっていること by @_mono
https://link.medium.com/KbOOF3GsRTIntroduction to widgets
https://flutter.io/docs/development/ui/widgets-introWidget inspector
https://flutter.io/docs/development/tools/inspector
以下では、MyHomePage内ではListViewの生成は行わずに、MyHomePageList内で行うようにしています。
Scaffoldを提供するWidgetは以下のようになります。
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: MyHomePageList(),
);
}
}
MyHomePageListのbuild()は以下のようになります。
class _MyHomePageListState extends State<MyHomePageList> {
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemBuilder: (context, index) {
return _buildListItem(_items[index]);
},
itemCount: _items.length,
);
}
}
2.ListViewを提供するWidget内にて、PrimaryScrollController.of(context)を用いてScaffold内の_primaryScrollControllerを呼び出し、addListner()を実装する
PrimaryScrollController.of(context)を用いてScaffold内の_primaryScrollControllerを呼び出します。
initState()内では、contextを使った処理は行えなえません。
そのため、以下ではdidChangeDependencies()でScaffold内の_primaryScrollControllerの取得処理を行っています。
取得した_primaryScrollControllerに対して、前述したListViewを監視する処理をaddListener()で追加しています。
ScrollController _primaryScrollController;
@override
void didChangeDependencies() {
_primaryScrollController = PrimaryScrollController.of(context);
_primaryScrollController.addListener(() {
final maxScrollExtent = _primaryScrollController.position.maxScrollExtent;
final currentPosition = _primaryScrollController.position.pixels;
if (maxScrollExtent > 0 && (maxScrollExtent - 20.0) <= currentPosition) {
_addContents();
}
});
super.didChangeDependencies();
}
ListViewのcontrollerプロパティに対して、_primaryScrollControllerを設定します。
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _primaryScrollController,
itemBuilder: (context, index) {
return _buildListItem(_items[index]);
},
itemCount: _items.length,
);
}
ListViewのプライマリ操作を有効にする実装については以上です。
コード全体は以下に載せています。
まとめ
Flutterで無限スクロールを実装する際のTipsは以上です。
分かりにくい箇所や不明点などございましたら、私のTwitterアカウントまでお願いします。
