[Flutter] VGVのvery_good_infinite_listとRiverpodで無限スクロール

[Very Good Ventures(VGV)](https://verygood.ventures/) が無限スクロール用のパッケージを用意してくれてました。

- [very_good_infinite_list](https://pub.dev/packages/very_good_infinite_list)

今回もRiverpodと合わせて使ってみたいと思います。

:::note info
同じくVGV製のformzを扱った記事を以前書いてます :bow: 
[[Flutter] formz+riverpodを使ったフォームの実装例](https://qiita.com/wakamenod/items/9f1c3a04301167afc78f)
:::

早速ですがサンプルコードです ([Github](https://github.com/wakamenod/riverpod_very_good_infinite_scroll)):

``` dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';

part 'main.g.dart';

@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<String>> build() async {
    await Future.delayed(const Duration(seconds: 1));
    return List.generate(10, (i) => 'ToDo Item $i');
  }

  Future<void> fetchTodo() async {
    final previousState = await future;

    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await Future.delayed(const Duration(seconds: 1));
      return previousState +
          List.generate(10, (i) => 'ToDo Item ${previousState.length + i}');
    });
  }
}

void main() => runApp(const ProviderScope(child: MyApp()));

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Simple Example'),
        ),
        body: Consumer(
          builder: (BuildContext context, WidgetRef ref, Widget? child) {
            final AsyncValue<List<String>> val = ref.watch(todoListProvider);

            return InfiniteList(
              itemCount: val.value?.length ?? 0,
              isLoading: val.isLoading,
              onFetchData: ref.read(todoListProvider.notifier).fetchTodo,
              separatorBuilder: (context, index) => const Divider(),
              itemBuilder: (context, index) {
                return ListTile(
                  dense: true,
                  title: Text(val.value?[index] ?? ''),
                );
              },
            );
          },
        ),
      ),
    );
  }
}
```

![ezgif-3-383c3e1813.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/da885cef-3474-6c29-c524-ecdf74e5712d.gif)

例によって使いやすい印象です！
[formz](https://pub.dev/packages/formz)もそうですが、中の実装を覗いてみるとシンプルな作りになっているようです:eyes: 

幾つか`InfiniteList` のオプションを試してみます。

### スクロールの向き
`reverse: true`で、スクロールが逆向きになります。チャットっぽいUIにも使えそう

```dart
            return InfiniteList(
+             reverse: true,
              itemCount: val.value?.length ?? 0,
              isLoading: val.isLoading,
              onFetchData: ref.read(todoListProvider.notifier).fetchTodo,
              separatorBuilder: (context, index) => const Divider(),
              itemBuilder: (context, index) {
```

![ezgif-3-047651cf61.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/47780866-8ab3-9da1-6b18-729d31ab50e1.gif)

### ローディングのインジケータ
`loadingBuilder` でローディング用のインジケータの見た目を変更出来ました。
`centerLoading`は初期ロードのインジケータの位置の調整でしょうか... :thinking: 

```dart
            return InfiniteList(
+              loadingBuilder: (context) => +const Center(
+                  child: CircularProgressIndicator(
+                valueColor: AlwaysStoppedAnimation(Colors.purpleAccent),
+              )),
+             centerLoading: true,
              itemCount: val.value?.length ?? 0,
              isLoading: val.isLoading,
              onFetchData: ref.read(todoListProvider.notifier).fetchTodo,
              separatorBuilder: (context, index) => const Divider(),
              itemBuilder: (context, index) {
```

![ezgif-5-eae105e42c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/239aa29c-7ce6-f4f3-794a-1c35d67d46c1.gif)

### エラー表示

3回目のfetchでエラーが出るようにしておきます

```dart
  Future<void> fetchTodo() async {
    final previousState = await future;

    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await Future.delayed(const Duration(seconds: 1));
+      if (previousState.length >= 30) throw 'Unknown Error!!!';
      return previousState +
          List.generate(10, (i) => 'ToDo Item ${previousState.length + i}');
    });
```

`hasError` と `errorBuilder` を使うことで、エラー表示をカスタマイズできました：

```dart
            return InfiniteList(
+              hasError: val.hasError,
+              errorBuilder: (context) {
+                return Padding(
+                  padding: const EdgeInsets.all(8.0),
+                  child: Container(
+                    padding:
+                        const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
+                    decoration: BoxDecoration(
+                      borderRadius: BorderRadius.circular(8.0),
+                      color: Colors.redAccent,
+                    ),
+                    child: Text(
+                      val.error.toString(),
+                      style: const TextStyle(
+                        color: Colors.white,
+                        fontSize: 16.0,
+                      ),
+                    ),
+                  ),
+                );
+              },
              itemCount: val.value?.length ?? 0,
```
![ezgif-5-889f98339f.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597113/dbfee9ac-000e-77e4-ce1f-35898dc8eed2.gif)

## まとめ
`very_good_infinite_list`と`Riverpod` で無限スクロールを実装してみました。
`hasReachedMax` や `debounceDuration` などを調整すれば、実際の業務にも使えそうです :dolphin: 

もっと良い使い方があれば、ぜひ教えて下さい :bow: