🔖

【Flutter】Cloud Firestoreのデータを取得してListView.builderで表示する。

2022/02/02に公開

はじめに

あるWidgetの下にリストを表示したい時に、
ListViewを使うとはみ出してしまっていた表示が、ListView.builderを使って表示できたので
その時の内容をメモとして残したいと思います。

Firestoreの環境構築やデータをとってくる部分はKboyさんの動画を参考にしてます。
かなりわかりやすいのでこちらも参考にしてみてください。
【Flutter実践】ModelからFirestoreのデータを取得する(mapの使い方も) - YouTube

この記事がどなたかの参考になれば幸いです。
また、もし間違い等ございましたらご指摘いただけるとありがたいです。

目的

Cloud Firestoreにある本一覧のデータをとって、ListView.builderで画面に表示します。
provider(ChangeNotifierとConsumer)を使い、main.dartでUI、main_model.dartで状態管理という書き方でやってみてます。

ListView.builderを使う理由

The standard ListView constructor works well for small lists. To work with lists that contain a large number of items, it’s best to use the ListView.builder constructor.
標準的なListViewのコンストラクタは、小さなリストではうまく機能します。多数のアイテムを含むリストを扱うには、ListView.builder コンストラクタを使用するのがベストです。
Work with long lists | Flutter

環境

MacOS12.1 monterey
Dart SDK version: 2.14.4 (stable)

flutter doctor

[] Flutter (Channel stable, 2.5.3, on macOS 12.1 21C52 darwin-arm, locale ja-JP)
[] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[] Xcode - develop for iOS and macOS
[] Chrome - develop for the web
[] Android Studio (version 2020.3)
[] Android Studio (version 2020.3)
[] Connected device (2 available)

• No issues found!

Firestoreはアプリに導入済とします。
導入については説明しておりません。導入については以下参考にしてください。
【Flutter実践】ModelからFirestoreのデータを取得する(mapの使い方も) - YouTube
Flutter アプリに Firebase を追加する  |  Firebase Documentation

Firestore, providerバージョン

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  provider: ^6.0.2
  firebase_core: ^1.11.0
  cloud_firestore: ^3.1.6

Firestoreでは以下のデータを持っているとします。

  • コレクション 'books'
  • 各ドキュメントが持っているフィールド'title'
    ※画像にはcreatedAtがありますが、今回は使いません。

完成図

作る時の流れ

  1. Firebaseの初期化処理をかく
  2. Firestoreのデータを扱うクラスを別ファイルで作る
  3. modelを作る
  4. ListView.builderにmodelの処理を反映させる。

1. Firebaseの初期化処理をかく

Firebaseの初期化処理を以下のようにmain()に書きます。

main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

2. FirestoreのDocumentを扱うクラスを別ファイルで作る

Firestoreのドキュメントが持っているフィールドを扱うクラスを作ります。
今回は本を扱うのでBook classとします。
Bookが持っているフィールドを定義し、
コンストラクタでfirestoreのデータをこのクラスのフィールド'title'に渡すようにします。

book.dart
import 'package:cloud_firestore/cloud_firestore.dart';

// firestoreのドキュメントを扱うクラスBookを作る。
class Book {
  
  // ドキュメントを扱うDocumentSnapshotを引数にしたコンストラクタを作る
  Book(DocumentSnapshot doc) {
    // ドキュメントの持っているフィールド'title'をこのBookのフィールドtitleに代入
    title = doc['title'];
  }
  // Bookで扱うフィールドを定義しておく。
  String title;
}

3. modelを作る

Firestoreや、UIで表示するデータをやり取りするmodelを作ります。
やっていることは

  • Firestoreからデータ(コレクション)を持ってくる。
  • 持ってきたコレクションのデータを色々加工して、
    ListView.builderで使うためのBookのList booksを用意する。
main_model.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:corianderapp/book.dart';
import 'package:flutter/cupertino.dart';

class MainModel extends ChangeNotifier {
  // ListView.builderで使うためのBookのList booksを用意しておく。 
  List<Book> books = [];

  Future<void> fetchBooks() async {
    // Firestoreからコレクション'books'(QuerySnapshot)を取得してdocsに代入。
    final docs = await FirebaseFirestore.instance.collection('books').get();

    // getter docs: docs(List<QueryDocumentSnapshot<T>>型)のドキュメント全てをリストにして取り出す。
    // map(): Listの各要素をBookに変換
    // toList(): Map()から返ってきたIterable→Listに変換する。
    final books = docs.docs
        .map((doc) => Book(doc)) 
        .toList();
    this.books = books; 
    notifyListeners(); 
  }
}

4. ListView.builderにmodelの処理を反映させる。

main.dart(UI書いているところ。)の中でListView.builderにFirestoreのデータを反映させる。
ChangeNotifierProviderとConsumerを使ってmodelのデータを反映させる。

main.dart
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider<MainModel>(
        // createでfetchBooks()も呼び出すようにしておく。
        create: (_) => MainModel()..fetchBooks(),
        child: Scaffold(
          appBar: AppBar(
            title: Text('本一覧'),
          ),
          body: Consumer<MainModel>(
            builder: (context, model, child) {
              // FirestoreのドキュメントのList booksを取り出す。
              final books = model.books; 
              return ListView.builder(
                // Listの長さを先ほど取り出したbooksの長さにする。
                itemCount: books.length,
                // indexにはListのindexが入る。
                itemBuilder: (context, index) {
                  return ListTile(
                    // books[index]でList booksのindex番目の要素が取り出せる。
                    title: Text(books[index].title),
                  );
                },
              );
            },
          ),
        ),
      ),
    );
  }
}

出来上がったコード全体

main.dart
import 'package:corianderapp/main_model.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider<MainModel>(
        create: (_) => MainModel()..fetchBooks(),
        child: Scaffold(
          appBar: AppBar(
            title: Text('本一覧'),
          ),
          body: Consumer<MainModel>(
            builder: (context, model, child) {
              final books = model.books;
              return ListView.builder(
                itemCount: books.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(books[index].title),
                  );
                },
              );
            },
          ),
        ),
      ),
    );
  }
}
main_model.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:corianderapp/book.dart';
import 'package:flutter/cupertino.dart';

class MainModel extends ChangeNotifier {
  List<Book> books = [];

  Future<void> fetchBooks() async {
    final docs = await FirebaseFirestore.instance.collection('books').get();
    final books = docs.docs.map((doc) => Book(doc)).toList();
    this.books = books;
    notifyListeners();
  }
}
book.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class Book {
  Book(DocumentSnapshot doc) {
    title = doc['title'];
  }
  String title;
}

参考サイト

Firebase環境構築からデータをとってListViewにするところまで、ほぼ参考にしてます。
ただ少し古いので、Firestoreの書き方はエラーになり、変更が必要になります。
https://www.youtube.com/watch?v=xeaiMqPeyQk&t=1064s

https://docs.flutter.dev/cookbook/lists/long-lists

Discussion