Flutter/Riverpod でスライダー作成

# 環境
- MaxOS Monterey
- Flutter 2.8.1
- Dart 2.15.1

**Flutter&Riverpod** でスライダーを実装しました。
より良いコーディングやアーキテクチャなどあればコメント欄にお願いいたします！

# 完成形

![スライド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1519965/29458262-1ce7-d47d-578a-3e838ef52299.gif)

# フォルダ構成
>flutter_app/
>├ images/
>│　└ image.jpg 
>├ lib/
>│ ├ models/
>│ │　　└ image_slider.dart
>│ ├ providers/
>│ │　　└ image_sliders.dart
>│ ├ screens/
>│ │　　└ screen.dart
>│ └ main.dart
>└ pubspec.yaml

# 実装
## package
最新バージョンなど詳細は公式でご確認ください
・[carousel_slider](https://pub.dev/packages/carousel_slider)
・[hooks_riverpod](https://pub.dev/packages/hooks_riverpod)
・[flutter_hooks](https://pub.dev/packages/flutter_hooks)

```pubspec.yaml
carousel_slider: ^4.0.0
hooks_riverpod: ^1.0.3
flutter_hooks: ^0.18.1
```

## Main

- **Riverpod** を使用するため **main.dart** に **hooks_riverpod**
をインポート
- **MyApp()** を **ProviderScope()** で囲う

```main.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; // 追加
import './screens/slider.dart';

void main() {
  runApp(
    const ProviderScope( // 追加
      child: MyApp(),
    )
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: SliderScreen(),
    );
  }
}
```

## Model

```image_slider.dart
import 'package:flutter/cupertino.dart';

class ImageSlider {
  final Image image;

  ImageSlider({ required this.image });
}
```

## Provider
ドットをクリックした時に、画像を変更したいので
carousel_controller　のインポートとコントローラを定義

```image_sliders.dart
import 'dart:collection'; // UnmodifiableListView の為
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_controller.dart'; // 追加
import '../models/image_slider.dart';

class ImageSliders extends ChangeNotifier {
  // お好きな画像を定義
  final List<ImageSlider> _imageSliders = [
    ImageSlider(image: Image.asset('images/dog1.jpg')),
    ImageSlider(image: Image.asset('images/dog2.jpg')),
    ImageSlider(image: Image.asset('images/dog3.jpg')),
    ImageSlider(image: Image.asset('images/dog4.jpg')),
    ImageSlider(image: Image.asset('images/dog5.jpg')),
  ];

  int _currentPoint = 0;

  // 追加
  CarouselController buttonCarouselController = CarouselController();

  // 他ファイルで _imageSliders に対して、add などの処理を防ぐ?
  UnmodifiableListView<ImageSlider> get imageSliders {
    return UnmodifiableListView(_imageSliders);
  }

  int get currentPoint {
    return _currentPoint;
  }

  void setCurrentPoint(int index) {
    _currentPoint = index;
    notifyListeners(); // 変更を通知
  }
}
```

## View
### Riverpod を実装


- hooks_riverpod をインポート
- プロバイダーを定義
- extends を **HookConsumerWidget** に変更
- Widget build() の引数に、**WidgetRef ref** を追加

### スライダーを実装

- ref.read(imageSliderProvider).imageSliders で定義した画像を呼び出し
- **CarouselSlider(~)** でスライダー実装(詳しくは公式参照)

ref.watch, ref.read があるが、
変更を検知して表示を変更したいなら、**watch** (Dotsで使用)
呼び出すだけで良いなら、**read**
で使い分けるっぽい

```slider.dart
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart'; // 追加
import 'package:hooks_riverpod/hooks_riverpod.dart'; // 追加
import '../models/image_slider.dart';
import '../providers/image_sliders.dart';

// プロバイダー定義
final imageSliderProvider = ChangeNotifierProvider((ref) => ImageSliders());

class SliderScreen extends HookConsumerWidget { // 変更
  const SliderScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) { // 追加
    // 画像の読み込み
    final List<ImageSlider> _imageSliders = ref.read(imageSliderProvider).imageSliders;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Dog Slider'),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 30.0),
        child: Column(
          children: [
            CarouselSlider( // スライダー
              carouselController: ref.read(imageSliderProvider).buttonCarouselController, // DotsIndicator用
              options: CarouselOptions(
                enlargeCenterPage: true, // 真ん中の画像以外を少し小さく表示
                height: MediaQuery.of(context).size.height / 4, // 高さ（お好みで）
                viewportFraction: 0.75, // 画像の横幅
                autoPlay: true, // 自動スクロール
                autoPlayInterval: const Duration(seconds: 5), // 自動スクロールの間隔
                onPageChanged: (index, _) {
                  // 現在の画像の位置を設定
                  ref.read(imageSliderProvider).setCurrentPoint(index);
                },
              ),
              items: _imageSliders.map((imageSlider) {
                return Builder(
                  builder: (BuildContext context) {
                    return SizedBox(
                      width: MediaQuery.of(context).size.width,
                      child: imageSlider.image,
                    );
                  },
                );
              }).toList(),
            ),
            DotsIndicator(imageSliders: _imageSliders),
          ],
        ),
      ),
    );
  }
}
~~
続く
~~
```

### ドットインディケーター
- DotsをWidget で分けているが、どちらでも
- ref.watch(topSliderProvider).currentPoint で変更を検知し、カラーを変更
- .buttonCarouselController.animateToPage(index)でタップした箇所の画像へスライド

```slider.dart
~~
続き
~~
// ドット部分
class DotsIndicator extends HookConsumerWidget {
  const DotsIndicator({
    Key? key,
    required List<TopSlider> topSliders,
  }) : _topSliders = topSliders, super(key: key);

  final List<TopSlider> _topSliders;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: _topSliders.map((topSlider) {
        int index = _topSliders.indexOf(topSlider);
        return GestureDetector(
          child: Container(
            width: 8.0,
            height: 8.0,
            margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0),
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: ref.watch(topSliderProvider).currentPoint == index ?
                  const Color.fromRGBO(0, 0, 0, 0.9) : const Color.fromRGBO(0, 0, 0, 0.4),
            ),
          ),
          onTap: () => ref.read(topSliderProvider).buttonCarouselController.animateToPage(index),
        );
      }).toList(),
    );
  }
}
```

これで動くはず！