【Flutter】Segmented buttons を実装する

##  初めに
Material Design3 の Components には [Segmented buttons](https://m3.material.io/components/segmented-buttons/overview) という項目があります。
しかし、Flutter の Widget には Segmented buttons が用意されていないため、今回はそれを実装してみたいと思います。

## 記事の対象者
+ Flutter と Riverpod の基礎理解ができている方
+ アプリ内で Segmented button を実装する方
+ Material Design3 に記載されていた内容をそのまま実装したい方

## 準備
今回は [material_segmented_control パッケージ](https://pub.dev/packages/material_segmented_control)を使って Segmented button を実装します。

### パッケージの追加
まずは [material_segmented_control パッケージ](https://pub.dev/packages/material_segmented_control) を「 pubspeck.yaml 」に記述します。
パッケージのバージョンは、特に制約がなければ最新のバージョンで問題ありません。

```yaml: pubspeck.yaml
dependencies:
  flutter:
    sdk: flutter

　　　　material_segmented_control: ^4.0.0
```
Pub get をしてパッケージの準備は完了です。

## 完成イメージ
以下のように Segmented button によって表示内容が切り替わるようにします。
![segmented_button.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/8c2f3e5a-cc1a-45d0-3edf-830d9acad846.gif)

## 全体コード
```dart:material_segmented_control.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_segmented_control/material_segmented_control.dart';

final currentSelectionProvider = StateProvider((ref) => 0);

class MaterialSegmentedControlSample extends ConsumerWidget {
  MaterialSegmentedControlSample({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final int currentSelection = ref.watch(currentSelectionProvider);
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              _customMaterialSegmentedControl(ref),
              const SizedBox(height: 30,),
              currentSelection == 0
                  ? Icon(
                      Icons.flutter_dash,
                      color: Colors.blue.shade300,
                      size: 100,
                    )
                  : currentSelection == 1
                      ? Icon(
                          Icons.flutter_dash,
                          color: Colors.green.shade300,
                          size: 100,
                        )
                      : Icon(
                          Icons.flutter_dash,
                          color: Colors.red.shade300,
                          size: 100,
                        ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);
    return MaterialSegmentedControl(
      children: _children,
      selectionIndex: currentSelection,
      borderColor: Colors.grey,
      selectedColor: currentSelection == 0
          ? Colors.blue.shade100
          : currentSelection == 1
              ? Colors.green.shade100
              : Colors.red.shade100,
      unselectedColor: Colors.white,
      borderRadius: 50.0,
      verticalOffset: 8.0,
      onSegmentChosen: (int index) {
        currentSelectionNotifier.state = index;
      },
    );
  }

  final Map<int, Widget> _children = {
    0: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Blue",
        style: TextStyle(color: Colors.black),
      ),
    ),
    1: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Green",
        style: TextStyle(color: Colors.black),
      ),
    ),
    2: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Red",
        style: TextStyle(color: Colors.black),
      ),
    ),
  };
}
```
以下の順番でそれぞれ詳しく解説します。
1.  [MaterialSegmentedControlSample](#materialsegmentedcontrolsample)
2.  [_customMaterialSegmentedControl](#_custommaterialsegmentedcontrol)
3.  [_children](#_children)

## MaterialSegmentedControlSample
```dart:material_segmented_control.dart
final currentSelectionProvider = StateProvider((ref) => 0);
```
このコードでまず選択されている Segmentedbutton ( 以下ボタン ) の index を監視するための StateProvider を作成しています。
初期値は0に指定しています。
<br>

```dart:material_segmented_control.dart
final int currentSelection = ref.watch(currentSelectionProvider);
```
このコードでは先ほど作成した StateProvider を ConsumerWidget 内で使えるように、読み取った値を `currentSelection` という変数に代入しています。
<br>

```dart:material_segmented_control.dart
 _customMaterialSegmentedControl(ref),
```
`Column`内のこのコードで `_customMaterialSegmentedControl()` という自分で定義した Widget を表示させています。
この Widget については [こちら](#_custommaterialsegmentedcontrol)で解説しています。
<br>

```dart:material_segmented_control.dart
currentSelection == 0
  ? Icon(
      Icons.flutter_dash,
      color: Colors.blue.shade300,
      size: 100,
    )
  : currentSelection == 1
    ? Icon(
        Icons.flutter_dash,
        color: Colors.green.shade300,
        size: 100,
      )
    : Icon(
        Icons.flutter_dash,
        color: Colors.red.shade300,
        size: 100,
      ),
```
このコードでは三項演算子をネストにして `currentSelection` の値に応じて表示させるアイコンの色を変更しています。
<br>

## _customMaterialSegmentedControl

```dart:material_segmented_control.dart
  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);
    return MaterialSegmentedControl(
      children: _children,
      selectionIndex: currentSelection,
      borderColor: Colors.grey,
      selectedColor: currentSelection == 0
          ? Colors.blue.shade100
          : currentSelection == 1
              ? Colors.green.shade100
              : Colors.red.shade100,
      unselectedColor: Colors.white,
      borderRadius: 50.0,
      verticalOffset: 8.0,
      onSegmentChosen: (int index) {
        currentSelectionNotifier.state = index;
      },
    );
  }
```
上のコードが先述した`_customMaterialSegmentedControl()` の内容です。
以下で内容を詳しくみていきます。
<br>

```dart:material_segmented_control.dart
  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);
```
このコードではまず Widget 内で Riverpod を使えるように `ref` を引数として受け取り、現在選択されているボタンを監視するための `currentSelectionProvider` を読み取っています。
<br>

```dart:material_segmented_control.dart
    return MaterialSegmentedControl(
      children: _children,
      selectionIndex: currentSelection,
      borderColor: Colors.grey,
      selectedColor: currentSelection == 0
          ? Colors.blue.shade100
          : currentSelection == 1
              ? Colors.green.shade100
              : Colors.red.shade100,
      unselectedColor: Colors.white,
      borderRadius: 50.0,
      verticalOffset: 8.0,
      onSegmentChosen: (int index) {
        currentSelectionNotifier.state = index;
      },
    );
```
この部分が [material_segmented_control パッケージ](https://pub.dev/packages/material_segmented_control) を使って実装できる部分です。
<br>

```dart:material_segmented_control.dart
children: _children,
```
このコードでボタンの子要素を指定しています。
なお、 `children` として指定できるのは Map 型のみです。
navigation_bar などはアイコンやテキストのリストで指定していたので、Map 型で指定するのは珍しいと言えるのではないでしょうか。
_children は[こちら](#_children)で解説しています。
<br>

```dart:material_segmented_control.dart
selectionIndex: currentSelection,
```
このコードで、現在選択されているボタンのインデックスを切り替えています。
このように指定することで、 `currentSelection` の値が切り替わった際に選択されているボタンが切り替わります。
<br>

```dart:material_segmented_control.dart
borderColor: Colors.grey,
```
このコードではボタンの枠線の色を指定しています。
何も指定しなければグレーの枠線になります。
<br>

```dart:material_segmented_control.dart
selectedColor: currentSelection == 0
  ? Colors.blue.shade100
  : currentSelection == 1
    ? Colors.green.shade100
    : Colors.red.shade100,
```
このコードではボタンが選択されている時の色を `currentSelection` の値をもとに三項演算子で変更しています。
「完成イメージ」ではボタンが選択された状態でもテキストは黒色のままでしたが、本体は `selectedColor` を指定するとボタンの背景色とテキストの両方の色が変更されます。

何も指定しなかった場合は以下のように青色になります。
![スクリーンショット 2022-09-10 14.48.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/c0e009cd-8f26-cdcd-1644-0e5a22afc6bb.png)
<br>

```dart:material_segmented_control.dart
unselectedColor: Colors.white,
```
このコードではプロパティの名前通り、選択されていないボタンの色を指定します。
コードでは白色にしていますが、何も指定しなかった場合は以下のように黒っぽい色になります。
![スクリーンショット 2022-09-10 14.50.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/1145b3ed-dfd5-786b-289a-a42dcc3dbd81.png)
<br>

```dart:material_segmented_control.dart
borderRadius: 50.0,
```
`borderRadius` ではボタンの角の丸みを調整しています。

`borderRadius: 0,`
このように指定すると以下のように全く丸みがなくなります。
![スクリーンショット 2022-09-10 14.54.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/9c1dccb5-1c53-115e-ecd5-b3b75bde5edf.png)
<br>


```dart:material_segmented_control.dart
verticalOffset: 8.0,
```
このコードではボタンの子要素の垂直方向の Padding にあたる空白を調整しています。

`verticalOffset: 30.0,`
このように値を大きくしてみると、以下のように子要素であるテキストとボタンの枠線の間が垂直方向に大きくなることがわかります。
![スクリーンショット 2022-09-10 14.58.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/33d68532-0bd2-2d3a-9dfb-a824e2da7686.png)
<br>

```dart:material_segmented_control.dart
onSegmentChosen: (int index) {
  currentSelectionNotifier.state = index;
},
```
このコードではボタンが選択された時の処理を記述しています。
引数として `index` を受け取り、`currentSelectionNotifier.state` に代入しています。

`currentSelectionNotifier.state` が変更されることにより、選択されているボタンのインデックスが変更され、ボタンが切り替わります。

## _children

```dart:material_segmented_control.dart
  final Map<int, Widget> _children = {
    0: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Blue",
        style: TextStyle(color: Colors.black),
      ),
    ),
    1: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Green",
        style: TextStyle(color: Colors.black),
      ),
    ),
    2: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Red",
        style: TextStyle(color: Colors.black),
      ),
    ),
  };
```
このコードではボタンの子要素にあたる Widget を記述しています。
`MaterialSegmentedControl()` の子要素は全て Map型で指定する必要があるため、ボタンのインデックスと表示内容がセットになった Map型が `_children` に代入されています。

```
final Map<int, Widget> _children = {
  ボタンのインデックス : ボタンで表示させる Widget 
}
```

以上です！

## あとがき
最後まで読んでいただきありがとうございました。

今回はボタンに応じて表示内容を変更させるために三項演算子を使用しました。
よく考えてみれば当たり前のことなのですが、if文における else if が三項演算子でも実装できると分かり、まだまだ知らないことだらけなのだと実感しました。

参考にしていただければ幸いです。
誤っている箇所があればご指摘いただければ幸いです。

### 参考にしたサイト
https://m3.material.io/components/segmented-buttons/overview

https://pub.dev/packages/material_segmented_control