[Flutter] TabBarを使ってオシャレなラジオボタン風な何かを作る

# 何を言っているのか
これを作ります。

![flutter_radio_indicator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526994/d4ee3228-abce-503e-71e2-bf0dcc932566.gif)

このような 選択したときに移動するようなアニメーションが出るラジオボタン？(複数の選択肢から1つ選ばせるボタン)を作っていきます。
ラジオボタンで調べたときにあんまり出てこなかったので記録。

# 作り方
## 使用するパッケージ
[tab_indicator_styler | Flutter Package](https://pub.dev/packages/tab_indicator_styler)

こちらのパッケージは、インジケーター（点とかアンダーラインとかの選択したときに移動するやつ)のよくあるスタイルが1つになったパッケージです。

## 使い方
* `TabController`の初期化に`TickerProviderStateMixin`を`Mixin`する必要があります。
* 選択中のインデックスをリアルタイムに表示したい場合などで、`setState`を用いて描画を更新しようとすると、選択中を示すエフェクトの移動アニメーションが打ち切られてしまうので、わざわざ`ValueListenableBuilder`を使ってインデックスを表示しています。（簡単なやり方がありそう。。。）
* 描画はしないけどインデックスだけ欲しい場合は`_tabController.index`で取得できます
* `TabBar.indicator`に指定している`DotIndicator`は、名前の通り 点のインジケーターです(gifの一番上のやつ)。
* 他にも2種類あります。（後述）

```dart
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

//  with TickerProviderStateMixin を忘れずに！！
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  TabController _tabController1;

  ValueNotifier<int> _currentIndex1 = ValueNotifier<int>(0);

  @override
  void initState() {
    super.initState();

    // TabControllerの初期化
    _tabController1 = TabController(length: _items.length, vsync: this);
  }

  final List<Widget> _items = [
    Tab(text: '111'),
    Tab(text: '222'),
    Tab(text: '333'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ValueListenableBuilder(
              valueListenable: _currentIndex1,
              builder: (BuildContext context, int index, Widget widget) {
                return Text('$index');
              },
            ),
            TabBar(
              controller: _tabController1,
              labelColor: Colors.black,
              unselectedLabelColor: Colors.grey,
              tabs: _items,
              indicator: DotIndicator(
                color: Colors.red[900],
                distanceFromCenter: 16,
                radius: 3,
                paintingStyle: PaintingStyle.fill,
              ),
              onTap: (int index) {
                _currentIndex1.value = index;
              },
            ),
          ],
        ),
      ),
    );
  }
}
```
## 他のインジケーター
点のインジケーターの他に、アンダーラインが動くインジケーターと、タブを四角で囲うインジケーターがあります。

また、`TabBar`を背景色をつけた`Container`で囲い、四角のインジケーターと組み合わせることで、gifの一番下のような感じにすることができます。


```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ValueListenableBuilder(
              valueListenable: _currentIndex1,
              builder: (BuildContext context, int index, Widget widget) {
                return Text('$index');
              },
            ),
            // 点のインジケーター
            TabBar(
              controller: _tabController1,
              labelColor: Colors.black,
              unselectedLabelColor: Colors.grey,
              tabs: _items,
              indicator: DotIndicator(
                color: Colors.red[900],
                distanceFromCenter: 16,
                radius: 3,
                paintingStyle: PaintingStyle.fill,
              ),
              onTap: (int index) {
                _currentIndex1.value = index;
              },
            ),
            Divider(),
            ValueListenableBuilder(
              valueListenable: _currentIndex2,
              builder: (BuildContext context, int index, Widget widget) {
                return Text('$index');
              },
            ),
            // アンダーラインのインジケーター
            TabBar(
              controller: _tabController2,
              labelColor: Colors.black,
              unselectedLabelColor: Colors.black,
              tabs: _items,
              indicator: MaterialIndicator(
                color: Colors.green[900],
                topLeftRadius: _radius,
                topRightRadius: _radius,
                bottomLeftRadius: _radius,
                bottomRightRadius: _radius,
              ),
              onTap: (int index) {
                _currentIndex2.value = index;
              },
            ),
            Divider(),
            ValueListenableBuilder(
              valueListenable: _currentIndex3,
              builder: (BuildContext context, int index, Widget widget) {
                return Text('$index');
              },
            ),
            // 矩形のインジケーター（角を丸くしたり、アウトラインにすることもできます）
            TabBar(
              controller: _tabController3,
              unselectedLabelColor: Colors.black,
              tabs: _items,
              indicator: RectangularIndicator(
                color: Colors.blue[900],
                topLeftRadius: _radius,
                topRightRadius: _radius,
                bottomLeftRadius: _radius,
                bottomRightRadius: _radius,
              ),
              onTap: (int index) {
                _currentIndex3.value = index;
              },
            ),
            Divider(),
            ValueListenableBuilder(
              valueListenable: _currentIndex4,
              builder: (BuildContext context, int index, Widget widget) {
                return Text('$index');
              },
            ),
            // 矩形インジケーターの応用
            Container(
              decoration: BoxDecoration(
                color: Colors.orange[900],
                borderRadius: BorderRadius.circular(_radius),
              ),
              child: TabBar(
                controller: _tabController4,
                labelColor: Colors.black,
                unselectedLabelColor: Colors.white,
                tabs: _items,
                indicator: RectangularIndicator(
                  color: Colors.orange[100],
                  topLeftRadius: _radius,
                  topRightRadius: _radius,
                  bottomLeftRadius: _radius,
                  bottomRightRadius: _radius,
                  horizontalPadding: 4,
                  verticalPadding: 4,
                ),
                onTap: (int index) {
                  _currentIndex4.value = index;
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
```