【Flutter】ListViewを無限ループスクロールできるようにしてユーザーを幸せにする。

どうも、個人でアプリ開発をしているYuKiOです。

「最高にかっこいいメモアプリHacker Memo」や「社会や街の安全に貢献するためのプラットフォームアプリみんなのチカラ」などアプリを15本リリースしています。

今回の記事は、ListViewで作成したボタン一覧を無限ループでスクロールする方法を紹介したいと思います。

色々調べましたが、なかなか良い解決策がなかったので記事にしてみました。

もし、もっと楽な方法があれば、教えて欲しいです。

## 実現したかったこと
水平にスクロールして選択できるカテゴリ選択ボタンリストがあります。左右どちらにスクロールしても、前後が繋がってリストが表示され続ける無限ループのに表示したい考えました。無限ループにすることで、両端のボタンに素早くアクセスできるようになることで快適になり、ユーザーが幸せになると考えたからです。

例えばリストのアイテム数が決まっている場合や、そこまで数が多くない（多くても20くらい）ことが想定される場合に限定される要望だと思いますが、以下のようにカテゴリ選択で今回は実装しました。

![IMG_2094.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228439/880d7e65-3cc4-5afa-cdcc-e3825f3c000d.jpeg)

例えば、SNSのタイムラインのようにリストの数が膨大な場合や長さが予測できない場合は、ループせずに最後まで行ったら、トップに戻るを表示してあげたほうがいいかなと考えています。

## 条件付き無限ループにしたいだけなら・・・
無限ループさせるだけであれば、以下で実現できます。


```category_list.dart
 final items = ["a","b","c","d","e","f"];
 final scrollController = ScrollController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Infinite Roop ListView'),
      ),
      body: ListView.builder(
        controller: ScrollController(),
        itemBuilder: (context, index) {
          final actualIdx = (index - startIdx) % items.length;
          return ListTile(title: Text(items[actualIdx]));
        },
      ),
    );
  }
```

### これじゃない感・・・
お気づきかもしれませんが、この場合は右には無限にスクロールができても、最初から左にはスクロールできません。

必要なのは左右の無限ループスクロール。


## そもそも、無限にこだわる必要はあるのか？
無限という言葉についつい憧れてしまいますが、本当に無限である必要はあるのか？例えば、何か10個程度のカテゴリリストの中を探している時に、20周も30周もしますか？って話です。

しないですよね。

ということで、以下の方法の仕様としました。

・リストがスクロール可能なら、同じリストを10個ほど連結。
・最初は真ん中あたりに飛ばす。
・端に到達したら、真ん中に飛ばす。


``` category_list.dart
List<CategoryModel> categoryModels = []
//useEffectやinitStateで処理
      if (scrollController.position.maxScrollExtent >
            scrollController.position.minScrollExtent) {
          List<CategoryModel> combinedList = [];

          for (int i = 0; i < 10; i++) {
            combinedList.addAll(items);
          }
          categoryModels = combinedList;
        } else {
          categoryModels = items;
        }

```


ビルド後に真ん中あたりに飛ばします。事前にitemリストから該当するカテゴリのindexを習得しておいて、
前にあるリストの長さ分のボタン幅、現在の表示されているカテゴリ一覧のボタン幅を足して、そこまでスクロールさせます。このあたりはレイアウトによって適宜調整してください。

```category_list.dart

  scrollController.jumpTo(
            (buttonWidth * (items.length * 4)) +
                 (buttonWidth * currentIndex);
```

左右には複数リスト分の余裕がありますので、進んだり戻ったりするのであれば、限界値まで来ることは少ないと思います。とはいえ、ユーザーが何も意識せずに端まで届いてしまった場合、進まないとバグと勘違いされる可能性があります。

そこで、端に到達したタイミングで、再度真ん中まで飛ばします。

```category_list.dart

    _scrollListener() {

      // 右の端に到達した場合
      if (scrollController.offset >=
              scrollController.position.maxScrollExtent - 10 &&
          !scrollController.position.outOfRange) {

        //上記のジャンプ処理と同じ。
      }
      // 左の端に到達した場合
      if (scrollController.offset <=
              scrollController.position.minScrollExtent + 10 &&
          !scrollController.position.outOfRange) {
         //上記のジャンプ処理と同じ。
    }

    //useEffectやinitStateで処理
      scrollController.addListener(_scrollListener);
        

```
正直、この処理が走ると少しカクついたような印象を受けますが、一般ユーザーがスクロールしている中でほとんど違和感ないレベルかなと考えています。

あまりにも端で処理が走ると違和感が出るので、端に到達する少し前に、処理が走るようにしています。これも実際に使ってみて、どの程度が違和感ないかチェックしてみてください。

アプリ開発などについてツイートしています。よかったらご覧ください。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">HackerMemoを大幅にアップデートしたので、<br><br>海外向けのPVを作ってみました😆<br><br>夜なべして頑張って作ったので、拡散頂けたら泣いて喜びます😭🙏<br><br>音有りがオススメです！<br><br>元ネタわかる人いるかな〜🤔<br><br>アプリはこちら🔽<br>■iPhoneの方<a href="https://t.co/6T10L94Ld1">https://t.co/6T10L94Ld1</a><br><br>■Androidの方<a href="https://t.co/uBevCXrNw2">https://t.co/uBevCXrNw2</a> <a href="https://t.co/wiGLv46kaG">pic.twitter.com/wiGLv46kaG</a></p>&mdash; YuKiO｜アプリ個人開発｜Flutter × Firebase (@oo_forward) <a href="https://twitter.com/oo_forward/status/1348103082769453056?ref_src=twsrc%5Etfw">January 10, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>