はじめに
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アカウントまでお願いします。