【Flutter】ヘッダー付きのリスト


# はじめに


ヘッダー付きのリストの実装をいくつか紹介します。

ヘッダー付きリストといっても、下図のようにリストのスクロールに合わせてヘッダーもスクロールするパターンとヘッダーは常に固定表示させておくパターンがあると思うのでそれぞれ紹介してみます。


|  ヘッダーもスクロール  |  ヘッダー固定   |
| ---- | ---- |
|  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/e57d9467-42dc-0ba7-097a-21b638abe5da.gif" width="300px">  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/42163f11-293d-7a71-3f96-e7fa3ead12c8.gif" width="300px">  |


# 実装

## ヘッダー部分もスクロール

ヘッダー部分もスクロールするパターンについては以下3つの実装を紹介します。

1. CustomScrollView + SliverToBoxAdapter
2. CustomScrollView + SliverAppBar
3. ListViewの先頭にヘッダーを配置

### 1. CustomScrollView + SliverToBoxAdapter

`CustomScrollView`の`slivers`プロパティの先頭に`SliverToBoxAdapter`を配置してヘッダーとして扱う方法です。

リスト部分は`SliverList`等を使って実装します。


```dart
class HeaderBySliverToBoxAdapter extends StatelessWidget {
  const HeaderBySliverToBoxAdapter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const SliverToBoxAdapter(
          child: ColoredBox(
            color: Colors.blueGrey,
            child: SizedBox(
              height: 100,
              width: double.infinity,
              child: Center(
                child: Text('Header'),
              ),
            ),
          ),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
            childCount: 20,
                (BuildContext context, int index) {
              return ListTile(
                title: Text(index.toString()),
              );
            },
          ),
        ),
      ],
    );
  }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/e57d9467-42dc-0ba7-097a-21b638abe5da.gif" width="300px">





### 2. CustomScrollView + SliverAppBar


`CustomScrollView`の`slivers`プロパティの先頭に`SliverAppBar`を配置してヘッダーとして扱う方法です。

1と同様に、リスト部分は`SliverList`等を使って実装します。


```dart
class HeaderBySliverAppBar extends StatelessWidget {
  const HeaderBySliverAppBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const SliverAppBar(
          expandedHeight: 100,
          backgroundColor: Colors.blueGrey,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('Header'),
            centerTitle: true,
          ),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
            childCount: 20,
            (BuildContext context, int index) {
              return ListTile(
                title: Text(index.toString()),
              );
            },
          ),
        ),
      ],
    );
  }
}
```

SliverAppBarを使用するため、ヘッダー部分もスクロールするというより、ヘッダー部分は縮小して消えるような挙動になります。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/fe62dae4-b80a-915b-f36c-3ad12ee2a1d0.gif" width="300px">

### 3. ListViewの先頭にヘッダーを配置

`ListView`の先頭(indexが0)の時にヘッダーのWidgetを返すようにする方法です。

このような方法でも実現できますが、実際に実装する際は`index=0`はヘッダー部分が使用していることを考慮して、リスト部分のindexを調整する必要が出てくるためコードが煩雑になり微妙かもです...


```dart
class HeaderByListView extends StatelessWidget {
  const HeaderByListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 20,
      itemBuilder: (BuildContext context, int index) {
        if (index == 0) {
          return const ColoredBox(
            color: Colors.blueGrey,
            child: SizedBox(
              height: 100,
              width: double.infinity,
              child: Center(
                child: Text('Header'),
              ),
            ),
          );
        } else {
          return ListTile(
            title: Text(index.toString()),
          );
        }
      },
    );
  }
}
```

挙動は1の「CustomScrollView + SliverToBoxAdapter」 と同じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/149117b6-c475-10cf-4c36-9f883915085c.gif" width="300px">




## ヘッダー固定

ヘッダーを固定するパターンについては以下2つを紹介します。
1. Columnで並べる
2. CustomScrollView + SliverAppBar + pinnedプロパティ

### 1. Columnで並べる


単純に`Column`でヘッダー部分と`ListView`を並べて実装する方法です。

`Column`の中で`ListView`を配置するため、`ListView`は`Expanded`等で囲っておく必要があることに注意。

```dart
class PinnedHeaderByColumn extends StatelessWidget {
  const PinnedHeaderByColumn({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ColoredBox(
          color: Colors.blueGrey,
          child: SizedBox(
            height: 100,
            width: double.infinity,
            child: Center(
              child: Text('Header'),
            ),
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: 20,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(
                title: Text(index.toString()),
              );
            },
          ),
        )
      ],
    );
  }
}
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/42163f11-293d-7a71-3f96-e7fa3ead12c8.gif" width="300px">




### 2. CustomScrollView + SliverAppBar + pinnedプロパティ


ヘッダー部分もスクロールする方法で紹介した 「CustomScrollView + SliverAppBar」 において、 `SliverAppBar`の`pinned`プロパティに`true`にセットすることで、ヘッダー部分を固定表示させることが可能です。

```dart
class PinnedHeaderBySliverAppBar extends StatelessWidget {
  const PinnedHeaderBySliverAppBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const SliverAppBar(
          pinned: true,
          expandedHeight: 100,
          backgroundColor: Colors.blueGrey,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('Header'),
            centerTitle: true,
          ),
        ),
        SliverList(
          delegate: SliverChildBuilderDelegate(
            childCount: 20,
                (BuildContext context, int index) {
              return ListTile(
                title: Text(index.toString()),
              );
            },
          ),
        ),
      ],
    );
  }
}
```

SliverAppBarを使用している都合上縮小するような挙動をとります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/658739/e35b0caa-9a40-7a25-3e1d-0ea84c1ccfd6.gif" width="300px">


以上です。

# 参考

* [CustomScrollView - api.flutter.dev](https://api.flutter.dev/flutter/widgets/CustomScrollView-class.html)
* [SliverToBoxAdapter - api.flutter.dev](https://api.flutter.dev/flutter/widgets/SliverToBoxAdapter-class.html)
* [SliverAppBar - api.flutter.dev](https://api.flutter.dev/flutter/material/SliverAppBar-class.html)