見出し画像

特定の位置までのスクロールを実現する3つのパッケージ

はじめに

FlutterではスクロールUIを実現するためにListViewやGridViewなど便利なWidgetが用意されています。実際にこれらを使うと、iOS/Androidで実装するよりも簡単なのでは?と思うくらい楽にスクロールUIを構築できてしまいます。

しかしスクロールを操作することについてはFlutterはイマイチだなと思うことがあります。スクロールを操作するにはListViewやGridViewにScrollControllerを渡し、それを使って操作することになります。

例えば、何かしらのユーザーアクションによって任意の位置にスクロールさせたい時、ScrollControllerはanimateTo/jumpToメソッドを持っていてそれらを使うことになるかと思いますが、このanimateTo/jumpToメソッドはどちらも直接Widgetのoffsetを指定する方法でしかスクロールを操作することができません。​コンテンツの高さに規則性があるUIであれば計算で直接指定できるかもしれませんが、そうでない場合は半ば強引に取得するしかないでしょう。GlobalKeyをWidgetに渡してそこからoffsetを取得するという方法が一般的だと思います(他にあれば教えてください)。この手法が使えるならまだしも、ListView.builderのようにSliverChildBuilderDelegateを用いてUIをスクロールによって都度構築している場合、描画範囲外にあるWidgetのoffsetはGlobalKeyを用いても取得できません。

​先日僕はこの問題に直面し、これを解決するために色々な手法を探りました。この記事ではその過程で調べたscroll_to_index、indexed_list_view、scrollable_positioned_listという3つのパッケージを紹介して、それぞれどのような特徴があるのかと、なぜ自分のチームではscrollable_positioned_listを採用したのかを解説していきます。

調査した3つのパッケージ

まずこの記事における課題を「UIをスクロールによって都度構築している場合に、描画範囲外にあるWidgetまでスクロールするListViewの実現」とします(GridViewは調査していないので想定しません)。これを実現するために、「はじめに」で紹介したscroll_to_index、indexed_list_view、scrollable_positioned_listの3つのパッケージはいずれもoffsetではなくindexを指定して特定の位置までスクロールするアプローチをとっています。

それぞれ順に簡単に紹介していきます。

scroll_to_index

scroll_to_indexは、ListViewには手を加えず、ScrollControllerをimplementsしたAutoScrollControllerと、AutoScrollTagという一つ一つのitemをラップして使用するWidgetを使ってindexによるスクロールを実現します。ListViewにAutoScrollControllerを渡してスクロールを操作するわけですが、AutoScrollTagにもAutoScrollControllerを渡すことでindexに対するWidgetをAutoScrollController側に持たせる仕組みのようです。ただ他の2つのパッケージにはあるjumpToIndexの機能がscroll_to_indexにはありません。つまり、スクロールする際のアニメーションが必須になります。これが一つ癖かなと思います。

indexed_list_view

indexed_list_viewは、ListViewの代わりに使うIndexedListViewというWidgetと、ScrollControllerを継承したIndexedScrollControllerを使ってindexによるスクロールを実現します。個人的に面白いなと思ったのは、IndexedListViewがnegative方向とpositive方向のSliverChildDelegateを持ち、それを二つのViewportにそれぞれセットしてStackに突っ込むことでスクロールを実現しているところです。よく考えたな〜と思いました。しかしindexed_list_viewにはイマイチな点があって、それは両方向のinfinity scrollじゃないと使えないという点です。両方向のinfinity scrollはあまりない要件なので結構致命的なんじゃないかなと個人的に思います。

scrollable_positioned_list

scrollable_positioned_listはFlutterコアチームではないGoogleが提供しているflutter.widgetsというパッケージの中にある一つのライブラリです。ListViewの代わりに使うScrollablePositionedListとItemScrollController(ScrollControllerを継承も実装もしていない)を使うことでindexによるスクロールを実現します。ScrollControllerを使えないのでスクロールの監視ができないのかと思いきや、ItemPositionsNotifierというクラスを使って、itemごとのindexとスクロール方向の最大値に対する両端の割合を表すitemLeadingEdge/itemTrailingEdge(日本語下手すぎるので興味ある方は実際にコードの方に書いてあるコメントを読んでください)を持つItemPositionを監視することができます。これが結構癖がある部分かなと思います。

なぜscrollable_positioned_listを採用したのか

3つ説明しましたが、なぜ僕のチームでは3つ目のscrollable_positioned_listを採用したかというと、正直scrollable_positioned_listしか要件を満たすものがなかったからです。上で言及した通り、scroll_to_indexはjumpToIndexの機能がなく、僕のチームではそれが必要だったため不採用となりました。indexed_list_viewはinfinity scrollでないと使用できない制限が受け入れ難く、不採用となりました。

実はscrollable_positioned_listも不採用になりかけました。スクロールのoffsetが必要な要件があり、scrollable_positioned_listのItemPositionsNotifierではおおよそのoffsetは取得できましたが、正確なoffsetを取得することができなかったためです。これに対しては、NotificationListener<ScrollNotification>でoffsetをキャッチすることで解決することができました。

おわりに

この記事では「UIをスクロールによって都度構築している場合に、描画範囲外にあるWidgetまでスクロールするListViewの実現」という課題に対して、3つのパッケージを用いた解決策を紹介しました。他にもパッケージを知っている方や、もっと簡単な手法を知っている方は是非教えていただきたいです。

この記事で紹介した3つのパッケージの利用例サンプルをGitHubにあげているため、もしよかったらそちらも参考にしてみてください。


Twitterはこちら https://twitter.com/kitoko552