Help us understand the problem. What is going on with this article?

Flutterで無限スクロールを実装する際のTips

More than 1 year has passed since last update.

はじめに

Flutterで無限スクロールを実装する際のTipsというタイトルにしましたが、ScrollControllerを用いてリストを監視したい時に広く使えるTipsです。
ハマり所でもあると思いますので、本記事を書きました。

ListView / SliverList / SingleChildScrollView などで適用できます。

motinoring_listview.gif

サンプル
https://github.com/HeavenOSK/monitoring_scroll

実装方法

実装する際のポイントは以下の2点です。

  1. ListViewScrollControllerに対して、現在の表示位置を監視するListnerを追加する
  2. iOS風の「AppBar上部をタップすると、ListViewを初期位置まで戻す」操作を実現するために、Scaffoldを提供するWidgetとListViewを提供するWidgetを分離し、親子構造にする

1について

現在の表示位置を監視して、下端に達した時に新たなコンテンツを追加する処理を行います。

2について

Flutterでは、iOS風の「AppBar上部をタップすると、ListViewを初期位置まで戻す」操作(以下,プライマリ操作)がサポートされています。
しかし、ScrollContollerListViewを監視する処理を素直に実装すると、プライマリ操作が無効になります。

例)
https://stackoverflow.com/questions/46377779/flutter-implementing-a-infinitescroll-view

以下では、一旦iOS風のプライマリ操作のことを考慮せずに実装した例を載せたのち、なぜプライマリ操作が無効化されたかを踏まえた実装の例を載せます。

ListViewを監視する

実装順序は以下のようになります。

1. ScrollControllerを初期化する
2. ScrollControllerListnerを追加して、更新処理を実装する
3. 作成したScrollControllerを、監視対象のリストのcontrollerプロパティに設定する

リストの監視はScrollControllerを介して行います。
ScrollControllerには、positionプロパティがあり、以下の値が取得できます。

  • position.maxScrollExtent => ListView全体の下端位置
  • position.pixels => 現在の表示位置

これらの値を比較して、「下端位置から20pixelの位置に達したら、コンテンツを読み込む」というような処理を追加します。

コードは以下のようになります。

StatefulWidgetinitState()内で、ScrollControllerを初期化して、addListner()します。

my_home_page.dart
  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();
  }

上記で作成した_scrollControllerListViewcontrollerプロパティに設定します。

my_home_page.dart
ListView.builder(
  controller: _scrollController,
  itemBuilder: (context, index) {
    return _buildListItem(_items[index]);
  },
  itemCount: _items.length,
),

ちなみに、コンテンツの追加処理は以下のようにしています。
今回はコードを単純にするためにsetState()で更新処理を行っています。

my_home_page.dart
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内で実装されています。
ScaffoldappBar上部のTap操作を監視しており、appBar上部がTapされた際には内部の_primaryScrollControllerというScrollControllerを介してリストを初期位置に戻す操作を行います。

ScaffoldbodyにてListViewを実装する際、ListViewcontrollerプロパティやprimaryプロパティを指定しなければ、暗黙的にScaffold内の_primaryScrollControllerが適用されます。

つまり、上記のリストを監視する実装方法では、Scaffold内のprimaryScrollControllerではなく、独自に定義した_scrollControllerListViewに適用しているため、プライマリ操作が無効になってしまいました。

以下では、プライマリ操作を有効にする実装方法を説明します。

ListViewのプライマリ操作を有効にする

上記の通り、プライマリ操作を有効にするには、ListViewScaffold内の_primaryScrollControllerを適用する必要があります。

コードに以下の修正を加えます。

  1. Scaffoldを提供するWidgetとListViewを提供するWidgetを分離して、親子構造にする
  2. ListViewを提供するWidget内にて、PrimaryScrollController.of(context)を用いてScaffold内の_primaryScrollControllerを呼び出し、addListner()を実装する

1.Scaffoldを提供するWidgetとListViewを提供するWidgetを分離して、親子構造にする

PrimaryScrollController.of(context)を使用するためには、呼び出し時に既にScaffoldがWidget Tree内に存在している必要があります。
Scaffoldを提供するWidgetとListViewを提供するWidgetを分離して、親子構造にすることで、ListViewの生成前にはScaffoldbuild()が完了し、Widget Tree内に存在している状態を作ります。

(Widget Treeについてご興味がある方は、以下を参考にして下さい)
Flutter の Widget ツリーの裏側で起こっていること by @_mono
https://link.medium.com/KbOOF3GsRT

Introduction to widgets
https://flutter.io/docs/development/ui/widgets-intro

Widget inspector
https://flutter.io/docs/development/tools/inspector

以下では、MyHomePage内ではListViewの生成は行わずに、MyHomePageList内で行うようにしています。

Scaffoldを提供するWidgetは以下のようになります。

my_home_page.dart
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(),
    );
  }
}

MyHomePageListbuild()は以下のようになります。

my_home_page_list.dart
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()で追加しています。

my_home_page_list.dart
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();
}

ListViewcontrollerプロパティに対して、_primaryScrollControllerを設定します。

my_home_page_list.dart
@override
Widget build(BuildContext context) {
  return ListView.builder(
    controller: _primaryScrollController,
    itemBuilder: (context, index) {
      return _buildListItem(_items[index]);
    },
    itemCount: _items.length,
  );
}

ListViewのプライマリ操作を有効にする実装については以上です。
コード全体は以下に載せています。

https://github.com/HeavenOSK/monitoring_scroll

まとめ

Flutterで無限スクロールを実装する際のTipsは以上です。

分かりにくい箇所や不明点などございましたら、私のTwitterアカウントまでお願いします。

https://twitter.com/heavenOSK/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした