【Flutter】Sliverでスクロールした時に上にくっつくTabBarの実装する

# 上にくっつくTabBarとは
そもそも上にくっつくTabBarってなんやねんと思う方もいるかもしれません
ここではいわゆるこんな感じのやつです。(正しい呼び方があれば教えてください！)

| SliverAppBar | SliverPersistentHeader |
| ---- | ---- |
| <img width="250" alt="SliverAppBar" src="https://storage.googleapis.com/zenn-user-upload/f64b5b491ffe-20211211.gif"> | <img width="250" alt="SliverPersistentHeader" src="https://storage.googleapis.com/zenn-user-upload/f00235bed9ed-20211211.gif">|

https://note.com/kitoko552/n/n7da7c55ebab6

こちらの記事ではStickyTabBarと呼んでいました！

この実装方法を主に2つ紹介します

# 実装方法
1. SliverAppBarによる実装
2. SliverPersistentHeaderによる実装

# SliverAppBarによる実装
<img width="250" alt="SliverAppBar" src="https://storage.googleapis.com/zenn-user-upload/f64b5b491ffe-20211211.gif">

SliverAppBarについてはこちらの公式ドキュメントによくまとまっています。いろんな例も詳しく載っているのでぜひご覧ください！

https://api.flutter.dev/flutter/material/SliverAppBar-class.html

[NestedScrollView](https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html)を使用することによりSliverAppBar＋TabBarViewの実装を可能にしています。
ドキュメントによるとNestedScrollViewのheaderにSliverAppBar、bodyにTabBarViewを設定するという実装が一般的だそう。

```Dart:実装例
//DefaultTabControllerでTabの数を設定する
 DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
	  //headerSliverBuilder内でSliverAppBarを設定
            return <Widget>[
              const SliverAppBar(
                pinned: true,//trueの場合、スクロースしても上にAppBarが残る
                expandedHeight: 150,//拡大されている際のAppBarの高さ
                bottom: TabBar(
                  //AppBarと同様にbottomにTabBarを設定
                ),
              ),
            ];
          },
	  //bodyにTabBarViewを設定
          body: TabBarView(
            children: [
		//Tabごとにウィジェットを設定する
            ],
          ),
        ),
      ),

```

# SliverPersistentHeaderによる実装
<img width="250" alt="SliverPersistentHeader" src="https://storage.googleapis.com/zenn-user-upload/f00235bed9ed-20211211.gif">
 
そこのあなた！AppBarとTabBarの間に何か挟みたい時、ありますよね？？
そんな時に使うのがSliverPersistentHeaderです！！　

ということでまずは全体的に見ていきましょう

```Dart
Scaffold(
      //AppBarを設定する
      appBar: AppBar(),
      body: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              _headerSection(),//オレンジ色の部分
              _tabSection(),//TabBarの部分
            ];
          },
          body: TabBarView(
            children: [
            //省略
            ],
          ),
        ),
      ),
    );

```

## _headerSection()について
NestedScrollViewのheaderSliverBuilder内ではSliverとつくWidget群を入れる必要があります。そこで_headerSectionはSliverListを用いて作成しました。

```Dart:_headerSection
Widget _headerSection() {
  return SliverList(
    delegate: SliverChildListDelegate(
      [
      //ここをカスタマイズする
        Container(
          color: Colors.orangeAccent,
          height: 100,
          child: const Center(
            child: Text('headerSection'),
          ),
        ),
      ],
    ),
  );
}
```


## _tabSection()について
SliverとつくWidget群を入れる必要があるためSliverPersistentHeaderを用いてTabBarを設定するのですが、SliverPersistentHeaderDelegateが必要となります

```Dart
SliverPersistentHeader(
    delegate: //SliverPersistentHeaderDelegateが必要（TabBar部分）
  );
  ```

そこでSliverPersistentHeaderを継承したクラスを作成します

```Dart:SliverPersistentHeaderDelegateを継承したTabBar
class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  const _StickyTabBarDelegate({required this.tabBar});

  final TabBar tabBar;

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: Colors.white,
      child: tabBar,
    );
  }

  @override
  bool shouldRebuild(_StickyTabBarDelegate oldDelegate) {
    return tabBar != oldDelegate.tabBar;
  }
}
```

以上からSliverで使用できるTabBarができたので、_tabSectionはこのようになります

```Dart:_tabSection()
Widget _tabSection() {
  return const SliverPersistentHeader(
    pinned: true,
    delegate: _StickyTabBarDelegate(
      tabBar: TabBar(
        tabs: [
          Tab(
            text: '1',
          ),
          Tab(
            text: '2',
          )
        ],
      ),
    ),
  );
}
```


# 全体のコード
こちらにSliverPersistentHeaderによる実装のサンプルコードを置いておくので色々試してみてください！

```Dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              _headerSection(),
              _tabSection(),
            ];
          },
          body: TabBarView(
            children: [
              ListView.builder(
                itemCount: 10,
                itemBuilder: (context, index) {
                  return Center(
                    child: Text(
                      index.toString(),
                      style: const TextStyle(
                        fontSize: 100,
                      ),
                    ),
                  );
                },
              ),
              ListView.builder(
                itemCount: 10,
                itemBuilder: (context, index) {
                  return Center(
                    child: Text(
                      index.toString(),
                      style: const TextStyle(
                        fontSize: 100,
                      ),
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

//header部分
Widget _headerSection() {
  return SliverList(
    delegate: SliverChildListDelegate(
      [
        Container(
          color: Colors.orangeAccent,
          height: 100,
          child: const Center(
            child: Text('headerSection'),
          ),
        ),
      ],
    ),
  );
}

//TabBar部分
Widget _tabSection() {
  return const SliverPersistentHeader(
    pinned: true,
    delegate: _StickyTabBarDelegate(
      tabBar: TabBar(
        labelColor: Colors.black,
        tabs: [
          Tab(
            text: '1',
          ),
          Tab(
            text: '2',
          )
        ],
      ),
    ),
  );
}

//SliverPersistentHeaderDelegateを継承したTabBarを作る
class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  const _StickyTabBarDelegate({required this.tabBar});

  final TabBar tabBar;

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: Colors.white,
      child: tabBar,
    );
  }

  @override
  bool shouldRebuild(_StickyTabBarDelegate oldDelegate) {
    return tabBar != oldDelegate.tabBar;
  }
}

```