【Flutter】Firestoreから取得した値をSingleChildScrollView内でListView表示したいけど、画面全体でスクロールができない！？

# はじめに
タイトルクソ長ですいません・・・:sweat_smile:
初心者の方はこういった問題に頭を悩めせた経験があるのではないでしょうか？？
![スクリーンショット 2022-05-16 7.59.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/d0cfffa8-7571-59e5-1eb4-19853178f009.png)

ListViewを使いつつ、**ListViewの部分だけではなく画面全体でスクロール可能にしたい**！という場合は多いですよね。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">StreamBuilderを使わずListView（ListView.builderではない）だけで、Firestoreから取得した値をSingleChildScrollViewで表示！！！ <a href="https://t.co/546QL1ocgB">pic.twitter.com/546QL1ocgB</a></p>&mdash; げんと、アプリ&amp;web開発 (@gento34165638) <a href="https://twitter.com/gento34165638/status/1526149635957719040?ref_src=twsrc%5Etfw">May 16, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**「ListViewに対して`shrinkWrap: true`と`physics: NeverScrollableScrollPhysics()`をつけたらできるんじゃないの？？」**

https://qiita.com/tabe_unity/items/4c0fa9b167f4d0a7d7c2

https://note.com/hatchoutschool/n/n82f306aa3f26

そう思われるかもしれませんが、、、実はできない場合もあるのです！！！:expressionless:

# StreamBuilderを使ってListViewを表示すると、画面全体でスクロールできない！

```dart
              Expanded(
                child: StreamBuilder(
                    stream: FirebaseFirestore.instance
                        .collection('posts')
                        .doc(widget.postId)
                        .collection('comments')
                        .orderBy(
                          'datePublished',
                          descending: true,
                        )
                        .snapshots(),
                    builder: (context,
                        AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>>
                            snapshot) {
                      if (snapshot.connectionState == ConnectionState.waiting) {
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                      return ListView.builder(
                          itemCount: snapshot.data!.docs.length,
                          itemBuilder: (ctx, index) => CommentCard(
                                snap: snapshot.data!.docs[index],
                              ));
                    }),
              ),
```

👆の状態では**ListViewの部分だけ**スクロールできます。。

ここでもう一度

**「ListViewに対して`shrinkWrap: true`と`physics: NeverScrollableScrollPhysics()`をつけたらできるんじゃないの？？」**

を思い出してみましょう。

その通りにやってみると、**今度はスクロールできなくなります・・・**

```dart
return ListView.builder(
   shrinkWrap: true,
   physics: NeverScrollableScrollPhysics(),
   itemCount: snapshot.data!.docs.length,
   itemBuilder: (ctx, index) => CommentCard(
       snap: snapshot.data!.docs[index],
   ),
 );
```

そりゃそうです。。ListViewをスクロールできなくしている訳ですから。。。。

## なぜStreamBuilderを使ったListViewではできない？
これはどういうことが起こっているかというと（厳密には違うかもですが、イメージとして）
`ListView.builder()`は画面に表示されている周辺を徐々に描画するので、`physics: NeverScrollableScrollPhysics()`の設定によって描画が初期状態で止まっています。

**いわゆるLazy Load**を画面初期の状態で止めているので、それより下に画面をはみ出した要素については描画されない＝スクロールできない、という状況ですね・・・:expressionless:

なので、**`ListView.builder()`ではなく`ListView()`を使わないといけません。**

>ListViewはリストで表示させる要素の数が少ない・決まっている場合などに使用されます。
ListView.builder()はリスト化したいWidgetの数が多い・決まっていない場合などに使用されます。
https://flutternyumon.com/how-to-use-listview/

ではどうやって`ListView()`で実現させればいいでしょうか？

# Providerの`notifylisteners`を使う
イメージとしては👇のような流れです。

①Firestoreから値を取得
②Listに格納（Providerで監視する）
③`ListView()`で表示（この時点では表示させる要素の数が決まっている）
④ Listの値に変化があった場合`notifylisteners`で変更を通知
⑤`ListView()`を再描画

## Providerを入れる

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^1.15.0
  cloud_firestore: ^3.1.3
  firebase_auth: ^3.3.16
  provider: ^6.0.2
```


## Listに格納する値のmodelを定義

```dart:comment.dart
class Comment {
  Comment(this.commentId, this.datePublished, this.text, this.uid);
  String commentId;
  DateTime datePublished;
  String text;
  String uid;
}
```


## Listに格納する

```dart:comment_provider.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:lovedan/models/comment.dart';

class CommentProvider with ChangeNotifier {
  List<Comment>? comments;

  void fetchComments(String postId) async {
    final QuerySnapshot snapshot = await FirebaseFirestore.instance
        .collection('posts')
        .doc(postId)
        .collection('comments')
        .orderBy(
          'datePublished',
          descending: true,
        )
        .get();

    // Firestoreから取得した値をListに格納
    final List<Comment> _comments =
        snapshot.docs.map((DocumentSnapshot document) {
      Map<String, dynamic> data = document.data() as Map<String, dynamic>;
      final String commentId = data['commentId'];
      final DateTime datePublished = data['datePublished'].toDate();
      final String text = data['text'];
      final String uid = data['uid'];
      return Comment(commentId, datePublished, text, uid);
    }).toList();

    this.comments = _comments;

    notifyListeners();
  }
}
```

## MaterialAppの親にProviderを追加する

```dart:main.dart
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
        providers: [
          ChangeNotifierProvider(
            create: (_) => UserProvider(),
          ),
          ChangeNotifierProvider(
            create: (_) => CommentProvider(), // これを使用する
          ),
        ],
        child: MaterialApp(
```


## `ListView()`で表示

```dart
import 'package:flutter/material.dart';
import 'package:lovedan/models/comment.dart';
import 'package:lovedan/providers/comment_provider.dart';
import 'package:provider/provider.dart';

class CommentCard extends StatelessWidget {
  final String postId;
  const CommentCard({Key? key, required this.postId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CommentProvider>.value(
      value: CommentProvider()..fetchComments(postId),
      child: Consumer<CommentProvider>(
        builder: (context, model, child) {
          final List<Comment>? _comments = model.comments;

          if (_comments == null) {
            return CircularProgressIndicator();
          }

          final List<Widget> _widgets = _comments
              .map((_comment) => Column(
                    children: [
                      Container(
                        margin: EdgeInsets.symmetric(vertical: 50),
                        child: Text(
                          _comment.text,
                          style: Theme.of(context).textTheme.headline6,
                        ),
                      ),
                      const Divider(
                        height: 20,
                        thickness: 1,
                      ),
                    ],
                  ))
              .toList();

          return ListView(
            shrinkWrap: true,
            physics: NeverScrollableScrollPhysics(),
            children: _widgets,
          );
        },
      ),
    );
  }
}
```

あとはこの`CommentCard`の親要素を`SingleChildScrollView`にすれば、画面全体をスクロールできるようになりました！

`Consumer`を使うことで`notifyListeners`に通知されたときに最小限の範囲でリビルドしてくれます。
Listに値を追加したり、逆に減らしたりする処理のときに`notifyListeners`を呼ぶことで、動的に状態を変更させることもできます:wink:



# 参考

https://github.com/flutteruniv/book-list-sample/tree/8afeebe9fb86ff08593a69a53991a5534e8c9525