【備忘録】flutterのPageViewでドットインジケーター

## はじめに
flutterでドットインジケータ作ったので備忘録的に残します。
もっといい書き方や内容的に間違っているところはコメント・編集リクエスト等で **優しく** 教えてください.

[githubのリポジトリはこちら](https://github.com/you22fy/flutter_dot_indicator.git)

完成品
![画面収録 2023-10-30 13.53.30.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2230776/83305a9f-658c-bb5b-b498-3bde5622d81f.gif)

## 実装
### 完成コード
まずは完成コードを載せておきます。

<details><summary>完成コード</summary>

```dart:dot_indicator.dart
class PageViewWithDotIndicator extends HookWidget {
  const PageViewWithDotIndicator({super.key});

  @override
  Widget build(BuildContext context) {
    final pages = [
      const SimplePage(
        title: '0ページ',
        contentText: '0ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '1ページ',
        contentText: '1ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '2ページ',
        contentText: '2ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '3ページ',
        contentText: '3ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '4ページ',
        contentText: '4ページ目のコンテンツです',
      ),
    ];

    final pageController = usePageController();

    final currentIndex = useState(
      (pageController.positions.isNotEmpty ? pageController.page : 0)!.round(),
    );

    useEffect(
      () {
        void listener() {
          if (pageController.positions.isNotEmpty) {
            currentIndex.value = pageController.page!.round();
          }
        }

        pageController.addListener(listener);

        return () {
          pageController.removeListener(listener);
        };
      },
      [],
    );

    return Scaffold(
      appBar: AppBar(
        title: Text(
          '現在のpageIndex : ${currentIndex.value.toString()}',
        ),
      ),
      body: Stack(
        children: [
          PageView(
            controller: pageController,
            children: pages,
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.only(
                bottom: 32,
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  for (int i = 0; i < pages.length; i++)
                    Padding(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                      ),
                      child: Container(
                        width: 16,
                        height: 16,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.black.withOpacity(
                            currentIndex.value == i ? 1 : 0.1,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

```

</details>

### 詳細な実装
flutter_hooksを導入しましょう
```
flutter pub add flutter_hooks
```
HooksWidgetの雛形を作ります。
```dart:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class PageViewWithDotIndicator extends HookWidget {
  const PageViewWithDotIndicator({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        children: [],
      ),
    );
  }
}
```

適当にpageを作っておいてください
```dart
    final pages = [
      const SimplePage(
        title: '0ページ',
        contentText: '0ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '1ページ',
        contentText: '1ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '2ページ',
        contentText: '2ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '3ページ',
        contentText: '3ページ目のコンテンツです',
      ),
      const SimplePage(
        title: '4ページ',
        contentText: '4ページ目のコンテンツです',
      ),
    ];
```
一応SimplePageのコードも載せておきます！
<details><summary>SimplePage</summary>

```dart:simple_page.dart
import 'package:flutter/material.dart';

class SimplePage extends StatelessWidget {
  const SimplePage({
    required this.title,
    required this.contentText,
    super.key,
  });

  final String title;
  final String contentText;
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.amber[200],
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              contentText,
              style: TextStyle(
                fontSize: 30,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

</details>

次にPageControllerを作成します.
```dart:
            title: '4ページ',
        contentText: '4ページ目のコンテンツです',
      ),
    ];

    // これを追記
    final pageController = usePageController();
    
    return Scaffold(
      body: PageView(
```

現在のページ番号とページの変化を管理するuseState, useEffectを書きます.

```dart:
    final currentIndex = useState(
      (pageController.positions.isNotEmpty ? pageController.page : 0),
    );

    // ページの移動を管理
    useEffect(
      () {
        void listener() {
          if (pageController.positions.isNotEmpty) {
            currentIndex.value = pageController.page;
          }
        }

        pageController.addListener(listener);

        return () {
          pageController.removeListener(listener);
        };
      },
      [],
    );

```

また、ウィジェットの部分をいい感じにします。PageViewに対してStackでドットの列を重ねて実装しています。
page番号は0から始まるので、現在のページ番号とドットのインデックスiが一致する時に透明度が1になるように作成しています。


```dart:
    return Scaffold(
      appBar: AppBar(
        title: Text(
          '現在のpageIndex : ${currentIndex.value.toString()}',
        ),
      ),
      body: Stack(
        children: [
          PageView(
            controller: pageController,
            children: pages,
          ),
          Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.only(
                bottom: 32,
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  for (int i = 0; i < pages.length; i++)
                    Padding(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                      ),
                      child: Container(
                        width: 16,
                        height: 16,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Colors.black.withOpacity(
                            currentIndex.value == i ? 1 : 0.1,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          )
        ],
      ),
    );
```

**一旦動かしてみましょう！**
この状態で動かすと以下のgifのようになるかと思います。
![画面収録 2023-10-30 13.54.08.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2230776/83c872b8-d3ee-6a5e-b148-40ffdcc53e26.gif)

ページが切り替わっている途中にどのインジケータも点灯していないことがわかるかと思います。
pageの値はページの遷移中は小数値になるので、うまく表示されないようですね.

これを解決するために、currentPage.valueの値を四捨五入して必ず整数値になるように調整します。
以下のように書き換えてみてください.
```dart:
    final currentIndex = useState(
      (pageController.positions.isNotEmpty ? pageController.page : 0)!.floor(),
    );

    useEffect(
      () {
        void listener() {
          if (pageController.positions.isNotEmpty) {
            currentIndex.value = pageController.page!.floor();
          }
        }

        pageController.addListener(listener);

        return () {
          pageController.removeListener(listener);
        };
      },
      [],
    );

```
これでうまく動作するはずです！
## 終わりに
今回はドットインジケータを実装してみました。
もし皆さんの参考になれば幸いです🙏