【Riverpod】TabBarの選択に合わせてアニメーションする

## できたもの
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">FlutterのAnimationの記事書きました。<br>【Riverpod】TabBarの選択に合わせてアニメーションする<a href="https://t.co/fyGB9bKW2P">https://t.co/fyGB9bKW2P</a><a href="https://twitter.com/hashtag/flutter?src=hash&amp;ref_src=twsrc%5Etfw">#flutter</a> <a href="https://t.co/ojeQgSuTWN">pic.twitter.com/ojeQgSuTWN</a></p>&mdash; Yusuke Miyata (@yuskey38) <a href="https://twitter.com/yuskey38/status/1492098262068707328?ref_src=twsrc%5Etfw">February 11, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
## やったこと
1.（CustomTabBarを使うためのデータクラスを作る)
2.（CustomTabBarを作成してTabBarの見た目を変える)
3. 選択したTabのindexを取得してStateを更新し、特定のindexの場合にViewを表示する
4. Animationを追加する

### 1. （CustomTabBarを使うためのデータクラスを作る)

```dart
class TabItem {
  const TabItem({required this.tab, required this.view});
  final Tab tab;
  final Widget view;
}
``` 

### 2. （CustomTabBarを作成してTabBarの見た目を変える)
引数に1で作成したTabItemのリストとTabを選択した際にindexを取得するための関数を渡しています。

また、以下のようにカスタマイズしてみました。
- 背景色を白
- 選択中のTabの色を青
- 非選択の色を灰色
- Indicatorの色を青
- Indicatorの幅を短く

```dart
class CustomTabBar extends StatelessWidget {
  const CustomTabBar(
      {Key? key, required this.tabItems, required this.onSelected})
      : super(key: key);

  final List<TabItem> tabItems;
  final Function(int) onSelected;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: tabItems.length,
        child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
          Ink(
              color: Colors.white,
              child: TabBar(
                  onTap: (index) {
                    onSelected(index);
                  },
                  labelColor: Colors.blue,
                  unselectedLabelColor: Colors.black12,
                  indicator: UnderlineTabIndicator(
                      borderSide:
                          const BorderSide(color: Colors.blue, width: 3.0),
                      insets: EdgeInsets.symmetric(
                          horizontal: 16.0 * tabItems.length)),
                  tabs: tabItems.map((e) => e.tab).toList())),
          Expanded(
              child: TabBarView(
                  physics: const NeverScrollableScrollPhysics(),
                  children: tabItems.map((e) => e.view).toList())),
        ]));
  }
}
``` 

### 3. 選択したTabのindexを取得してStateを更新し、特定のindexの場合にViewを表示する
2で作成したCustomTabBarの`onSelected`でindexを取得し、viewModel経由でstateを更新しています。
selectedIndexが0の場合はcontainerのheightを0にして表示しないようにし、その他のindexの場合はheightを１００にして表示するようにしています。

```dart
class TestView extends HookConsumerWidget {
  const TestView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final viewModel = ref.read(tabViewModelProvider.notifier);
    final state = ref.watch(tabViewModelProvider);
    return MaterialApp(
        title: 'TestView',
        home: Scaffold(
            body: SafeArea(
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
              Container(
                  height: state.selectedIndex == 0 ? 0 : 100,
                  color: Colors.blue,
                  child: const Center(
                    child: Text(
                      "Tabbar with out Appbar",
                      style: TextStyle(
                          color: Colors.white, fontWeight: FontWeight.bold),
                    ),
                  )),
              Expanded(
                  child: CustomTabBar(
                onSelected: (index) {
                  viewModel.select(index);
                },
                tabItems: const [
                  TabItem(
                      tab: Tab(text: "LEFT"),
                      view: Center(child: Text("LEFT"))),
                  TabItem(
                      tab: Tab(text: "RIGHT"),
                      view: Center(child: Text("RIGHT")))
                ],
              ))
            ]))));
  }
}
```
```dart
final tabViewModelProvider =
    StateNotifierProvider.autoDispose<TabViewModel, TabState>((ref) {
  return TabViewModel();
});

class TabViewModel extends StateNotifier<TabState> {
  TabViewModel() : super(const TabState());

  void select(int index) {
    state = state.copyWith(selectedIndex: index);
  }
}
```
```dart
@freezed
class TabState with _$TabState {
  const factory TabState({@Default(0) int selectedIndex}) = _TabState;
}
```
### 4. Animationを追加する
Animationを追加するには`Container`を`AnimatedContainer`に変更し、`duration`を追加するだけです。

```dart
Container(
  height: state.selectedIndex == 0 ? 0 : 100,
    color: Colors.blue,
    child: const Center(
      child: Text(
        "Tabbar with out Appbar",
        style: TextStyle(
            color: Colors.white, fontWeight: FontWeight.bold),
      ),
    )),
```
↓

```dart
AnimatedContainer(
    duration: const Duration(milliseconds: 250),
    height: state.selectedIndex == 0 ? 0 : 100,
    color: Colors.blue,
    child: const Center(
      child: Text(
        "Tabbar with out Appbar",
        style: TextStyle(
            color: Colors.white, fontWeight: FontWeight.bold),
      ),
    )),
```