初めに
Material Design3 の Components には Segmented buttons という項目があります。
しかし、Flutter の Widget には Segmented buttons が用意されていないため、今回はそれを実装してみたいと思います。
記事の対象者
- Flutter と Riverpod の基礎理解ができている方
- アプリ内で Segmented button を実装する方
- Material Design3 に記載されていた内容をそのまま実装したい方
準備
今回は material_segmented_control パッケージを使って Segmented button を実装します。
パッケージの追加
まずは material_segmented_control パッケージ を「 pubspeck.yaml 」に記述します。
パッケージのバージョンは、特に制約がなければ最新のバージョンで問題ありません。
dependencies:
  flutter:
    sdk: flutter
    material_segmented_control: ^4.0.0
Pub get をしてパッケージの準備は完了です。
完成イメージ
以下のように Segmented button によって表示内容が切り替わるようにします。

全体コード
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),
      ),
    ),
  };
}
以下の順番でそれぞれ詳しく解説します。
MaterialSegmentedControlSample
final currentSelectionProvider = StateProvider((ref) => 0);
このコードでまず選択されている Segmentedbutton ( 以下ボタン ) の index を監視するための StateProvider を作成しています。
初期値は0に指定しています。
final int currentSelection = ref.watch(currentSelectionProvider);
このコードでは先ほど作成した StateProvider を ConsumerWidget 内で使えるように、読み取った値を currentSelection という変数に代入しています。
 _customMaterialSegmentedControl(ref),
Column内のこのコードで _customMaterialSegmentedControl() という自分で定義した Widget を表示させています。
この Widget については こちらで解説しています。
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 の値に応じて表示させるアイコンの色を変更しています。
_customMaterialSegmentedControl
  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() の内容です。
以下で内容を詳しくみていきます。
  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);
このコードではまず Widget 内で Riverpod を使えるように ref を引数として受け取り、現在選択されているボタンを監視するための 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;
      },
    );
この部分が material_segmented_control パッケージ を使って実装できる部分です。
children: _children,
このコードでボタンの子要素を指定しています。
なお、 children として指定できるのは Map 型のみです。
navigation_bar などはアイコンやテキストのリストで指定していたので、Map 型で指定するのは珍しいと言えるのではないでしょうか。
_children はこちらで解説しています。
selectionIndex: currentSelection,
このコードで、現在選択されているボタンのインデックスを切り替えています。
このように指定することで、 currentSelection の値が切り替わった際に選択されているボタンが切り替わります。
borderColor: Colors.grey,
このコードではボタンの枠線の色を指定しています。
何も指定しなければグレーの枠線になります。
selectedColor: currentSelection == 0
  ? Colors.blue.shade100
  : currentSelection == 1
    ? Colors.green.shade100
    : Colors.red.shade100,
このコードではボタンが選択されている時の色を currentSelection の値をもとに三項演算子で変更しています。
「完成イメージ」ではボタンが選択された状態でもテキストは黒色のままでしたが、本体は selectedColor を指定するとボタンの背景色とテキストの両方の色が変更されます。
unselectedColor: Colors.white,
このコードではプロパティの名前通り、選択されていないボタンの色を指定します。
コードでは白色にしていますが、何も指定しなかった場合は以下のように黒っぽい色になります。

borderRadius: 50.0,
borderRadius ではボタンの角の丸みを調整しています。
borderRadius: 0,
このように指定すると以下のように全く丸みがなくなります。

verticalOffset: 8.0,
このコードではボタンの子要素の垂直方向の Padding にあたる空白を調整しています。
verticalOffset: 30.0,
このように値を大きくしてみると、以下のように子要素であるテキストとボタンの枠線の間が垂直方向に大きくなることがわかります。

onSegmentChosen: (int index) {
  currentSelectionNotifier.state = index;
},
このコードではボタンが選択された時の処理を記述しています。
引数として index を受け取り、currentSelectionNotifier.state に代入しています。
currentSelectionNotifier.state が変更されることにより、選択されているボタンのインデックスが変更され、ボタンが切り替わります。
_children
  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 が三項演算子でも実装できると分かり、まだまだ知らないことだらけなのだと実感しました。
参考にしていただければ幸いです。
誤っている箇所があればご指摘いただければ幸いです。
参考にしたサイト
