😬

StreamBuilderとFutureBuilderって??firebaseに保存されているデータを表示させよう!

2023/10/04に公開

StreamBuilderとFutureBuilderを使ってfirebaseに保存されているデータを表示させよう!

どうもこんにちは!flutter初心者のつきゆびです🔰

この記事ではStreamBuilderとFutureBuilderの解説、firebase Cloud Firestoreに保存されている情報を取得する方法を解説してます!

まず初めは簡単にStreamBuilderとFutureBuilderについて軽く触れます!

StreamBuilder

Dartプログラミング言語とFlutterフレームワークで使用されるウィジェットの一種です。これは、アプリケーション内でデータの変更を監視し、それに応じてユーザーインターフェース(UI)を自動的に更新するために使われます。これを理解するために、以下のポイントを押さえておきましょう。

StreamBuilderの例え話

イメージしてみてください。あなたがお風呂に入りたいと思って、お湯をためるために蛇口を開けました。このとき、水がお風呂に入ってくるのを待つのと同じように、StreamBuilderはデータが入ってくるのを待ちます。

さて、お湯が入ってくると、あなたはお湯が入るたびに何かをするかもしれません。例えば、お湯がたまるたびに「お湯がたまってきた!」と言うかもしれません。StreamBuilderも同じように、データが変わるたびに何かをすることができます。

もしある日、お風呂のお湯が急に冷たくなったとしましょう。あなたはそれに気づき、急いで蛇口を閉めますよね?StreamBuilderも同じように、データに何か問題があると、それに反応して何かをすることができます。

簡単に言えば、StreamBuilderは情報の流れを監視して、情報が変わったときに何かをするFlutterの特別なツールなんです。お風呂の水道蛇口のように、情報が変わるたびに反応することができます。これにより、リアルタイムの情報をアプリに取り込んで、ユーザーエクスペリエンスを向上させることができるんです。

- Stream(ストリーム)とは何ですか?

ストリームは、データの連続的な流れと覚えておこう!例えば、ウェブカメラからのビデオデータや、センサーデータ、またはオンラインゲームのプレーヤーの動きなど、リアルタイムで更新されるデータを表現するために使われます。

- StreamBuilderの役割

StreamBuilderは、ストリームからのデータをリッスン(監視)し、データが変更されたときに自動的にUIを更新するのに役立ちます。例えば、Firestoreのデータベースからのリアルタイムデータ更新や、センサーデータの表示などに使用できます。

- 基本的なStreamBuilderの構造
StreamBuilder<T>(
  stream: データのストリーム,
  builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
    // ストリームから取得したデータを使ってUIを構築するコード
  },
)
  • Tはデータの型を表します。
  • streamプロパティには監視するデータのストリームを指定します。
  • builderプロパティ内では、snapshotオブジェクトを使ってデータをチェックし、UIを構築します。

- builder内の主なチェックポイント

  • snapshot.connectionState: データが読み込まれているか、エラーが発生しているか、データが存在しないかを確認します。
  • snapshot.hasError: エラーが発生している場合の処理を行います。
  • snapshot.hasData: データが存在する場合、それを使ってUIを構築します

FutureBuilder

FutureBuilderは、DartプログラムやFlutterアプリで非同期処理を行う際に役立つウィジェットです。非同期処理は、アプリが他のタスクを実行しながら待機せざるを得ない場合に使用されます。例えば、ネットワークからデータを取得したり、ファイルを読み書きしたりする場合などです。以下のポイントを理解すると、FutureBuilderの役割がわかります。

FutureBuilderの例え話

想像してください、夕食にピザを注文しましたが、そのピザが届くのを待たなければなりません。待っている間、ただ何もせずにいるわけにはいかないので、スマートフォンでゲームをすることにしました。

これを、待っている「未来の出来事」と考えてみましょう。あなたはその美味しいピザを本当に食べたいのですが、まだ玄関先には届いていません。これは、FutureBuilderの動作に似ています。FutureBuilderは、未来の出来事が起こるのを待ちながら別のことをするのに役立ちます。

だから、スマートフォンでゲームを始め、時々ドアを確認します。そして、ついにピザが届いたとき、スマートフォンを置いて、おいしい食事を楽しむことができ、友達を招待して一緒にピザを楽しむこともできます。

このたとえ話では:

  • ピザを注文することは、非同期の操作を開始することと似ています(インターネットからデータを取得するなど)。
  • スマートフォンでゲームをプレイすることは、待っている間に行うこと(読み込み中のインジケータを表示したり、別のタスクを実行したりすること)です。
  • ピザを確認することは、FutureBuilderが未来の出来事(データの取得など)が発生したかどうかを継続的にチェックする方法と似ています。
  • 最後に、ピザが届いたときに行動を起こすことができます(取得したデータを表示するなど)。
    したがって、FutureBuilderは、未来の出来事を待ちながら生産的で他のことをするのに役立つ、まるでそうしたいと思って待っている感覚に似ています。
- 非同期処理とは何ですか?

非同期処理は、アプリケーションが時間のかかるタスクを実行する際に、その完了を待たずに他の処理を続ける方法です。例えば、ウェブからデータをダウンロードする場合、データのダウンロードが完了するまで待つのではなく、他の操作を実行しながらダウンロードを進行させることができます。

- FutureBuilderの役割

FutureBuilderは、非同期処理の結果を待ちながら、その結果に基づいてユーザーインターフェース(UI)を構築するのに役立ちます。非同期処理が完了すると、その結果に基づいてUIを動的に更新します。
基本的なFutureBuilderの構造

- FutureBuilderウィジェットは、以下のように構成されます。
FutureBuilder<T>(
  future: 非同期処理の呼び出し,
  builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
    // 非同期処理の結果に基づいてUIを構築するコード
  },
)

  • Tは非同期処理の結果の型を表します。
    futureプロパティには非同期処理を呼び出すメソッドを指定します。
  • builderプロパティ内では、snapshotオブジェクトを使って非同期処理の結果をチェックし、UIを構築します。
- builder内の主なチェックポイント
  • snapshot.connectionState: 非同期処理の状態を確認し、完了したか、エラーが発生したかを判断します。
  • snapshot.hasError: エラーが発生した場合、適切なエラーメッセージを表示するなどの処理を行います。
  • snapshot.hasData: 非同期処理が成功し、データが利用可能な場合にUIを構築します。
    簡単に言えば、FutureBuilderは非同期処理の結果を待ちつつ、それが利用可能になったらそれに基づいてUIを構築するツールです。ネットワークリクエスト、データベースクエリ、ファイル読み込みなどの非同期操作を行う際に非常に役立ちます。初学者でも理解しやすい概念であり、Flutterアプリケーションの開発において頻繁に使用されます。

今回のコードに入っていきます!

今回はバスケットチームメンバーを募集する投稿をCloud Firestoreに保存しました

まずCloud Firestore今回のデータです


teamPostsにはバスケットチームメンバーを募集する投稿が保存されています

そしてusersにはユーザー情報とさらにmy_postsにはユーザーが投稿したチームメンバー募集投稿が保存されています

ゴールはCloud Firestoreに保存されているteamPostsの情報を取得してlistにして表示させる

全体のコードです
  child: StreamBuilder<QuerySnapshot>(
                        stream: PostFirestore.teamPosts
                            .orderBy("created_time", descending: true)
                            .snapshots(),
                        builder: (context, postSnapshot) {
                          if (postSnapshot.connectionState ==
                              ConnectionState.waiting) {
                            // データが読み込まれていない状態
                            return Center(
                              child: CircularProgressIndicator(), // ローディング中の表示
                            );
                          } else if (postSnapshot.hasError) {
                            print(postSnapshot.error);

                            // エラーが発生した場合
                            return Center(
                              child: Text(
                                "データの読み込み中にエラーが発生しました ${postSnapshot.error}",
                                style: TextStyle(color: Colors.white),
                              ),
                            );
                          } else if (!postSnapshot.hasData ||
                              postSnapshot.data!.docs.isEmpty) {
                            // データが存在しない場合
                            return Center(
                              child: Text(
                                "何も投稿がありません",
                                style: TextStyle(color: Colors.white),
                              ),
                            );
                          } else {
                            //投稿を繰り返し取得しpost_account_idを取得する
                            List<String> postAccountIds = [];
                            postSnapshot.data!.docs.forEach((doc) {
                              Map<String, dynamic> data =
                                  doc.data() as Map<String, dynamic>;
                              if (!postAccountIds
                                  .contains(data["post_account_id"])) {
                                postAccountIds.add(data["post_account_id"]);
                              }
                            });
                            return FutureBuilder<Map<String, Account>?>(
                                future: UserFirestore.getPostUserMap(
                                    postAccountIds),
                                builder: (context, userSnapshot) {
                                  //ユーザー情報の取得が完了しているかどうかをチェック
                                  if (userSnapshot.hasData &&
                                      userSnapshot.connectionState ==
                                          ConnectionState.done) {
                                    return ListView.builder(
                                      itemCount: postSnapshot.data!.docs.length,
                                      // データの長さを指定
                                      itemBuilder:
                                          (BuildContext context, int index) {
                                        Map<String, dynamic> data = postSnapshot
                                            .data!.docs[index]
                                            .data() as Map<String, dynamic>;

                                        return TeamPostWidget(
                                            data: data,
                                            id: postSnapshot
                                                .data!.docs[index].id);
                                      },
                                    );
                                  } else {
                                    return Center(
                                      child: CircularProgressIndicator(),
                                    );
                                  }
                                });
                          }
                        }),

中身を細く説明してきます

ステップ1: StreamBuilder ウィジェットの作成

child: StreamBuilder<QuerySnapshot>(
                        stream: PostFirestore.teamPosts
                            .orderBy("created_time", descending: true)
                            .snapshots(),
                        builder: (context, postSnapshot) {
//省略
}),
  • StreamBuilder<QuerySnapshot>: StreamBuilderウィジェットを作成しています。このウィジェットは、Firestoreからのデータの変更をリアルタイムに監視し、変更があるたびにUIを更新する役割を担います

  • stream:PostFirestore.teamPosts.orderBy("created_time", descending: true).snapshots(): このプロパティは、どのデータを監視するかを指定します。具体的には、PostFirestore.teamPosts はFirestore内のデータベースの中で teamPostsというコレクション(データのまとまり)を指し、.orderBy("created_time", descending: true)はデータを取得する際に created_time フィールドで降順にソート(並び替え)することを意味します。そして、.snapshots() はこのデータに対してリアルタイムなストリームを取得します。つまり、データが変更されるたびに通知を受け取ることができます。

  • builder: (context, postSnapshot): これは、Firestoreからのデータ変更に対応するUIを構築するためのコールバック関数を指定します。postSnapshot パラメータは、Firestoreからのデータのスナップショット(瞬間的な状態)を表します。このスナップショットを使用して、UIを動的に構築します

このコードは、Firestoreの teamPosts コレクションからデータをリアルタイムに取得し、そのデータをUIに反映するための基本的な設定を行っています。Firestore内のデータが変更されると、このStreamBuilderは自動的にUIを更新して新しいデータを表示します。

QuerySnapshot(クエリスナップショット)とは

Firestore(Firebaseのデータベースサービス)から取得したデータの一覧やコレクションを表すものです。
次の例を考えてみましょう。

想像してみてください。あなたが学校の図書館に行ったとします。図書館にはたくさんの本があり、それらの本は様々な本棚に整理されています。あなたが特定の本を見つけるためには、本棚の中から探す必要があります。Firestoreの「QuerySnapshot」は、この本棚の中身を表現したものと考えることができます。

ステップ2: データの読み込み状態を確認

if (postSnapshot.connectionState == ConnectionState.waiting) {
  // データが読み込まれていない状態の場合、ローディングアイコンを表示します。
  return Center(
    child: CircularProgressIndicator(),
  );
}
	

ステップ3: エラーが発生した場合の処理

else if (postSnapshot.hasError) {
  // エラーが発生した場合、エラーメッセージを表示します。
  print(postSnapshot.error);
  return Center(
    child: Text(
      "データの読み込み中にエラーが発生しました ${postSnapshot.error}",
      style: TextStyle(color: Colors.white),
    ),
  );
}	

ステップ4: データが存在しない場合の処理

else if (!postSnapshot.hasData || postSnapshot.data!.docs.isEmpty) {
  // データが存在しない場合、適切なメッセージを表示します。
  return Center(
    child: Text(
      "何も投稿がありません",
      style: TextStyle(color: Colors.white),
    ),
  );
}

ステップ5: ユーザー情報を取得するためのリストを作成

そして postSnapshot.data!.docs.forEach を使って、Firestoreの teamPostsコレクションから取得したドキュメント(投稿)を一つずつ処理しています。そして、各ドキュメント内のデータから "post_account_id" (投稿者のid)を取り出し、それを postAccountIds リストに追加しています。ただし、同じ "post_account_id" がすでにリスト内に存在する場合は重複して追加されないようにしています。

else {
  // 投稿を繰り返し取得し、post_account_idを取得するリストを作成します。
  List<String> postAccountIds = [];
  postSnapshot.data!.docs.forEach((doc) {
    Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
    if (!postAccountIds.contains(data["post_account_id"])) {
      postAccountIds.add(data["post_account_id"]);
    }
  });
}

どうして`postAccountIds`を作る必要なのか
  1. Firestore内には複数の投稿があります。
  2. これらの投稿には、それぞれ異なるユーザーによって作成されたものが含まれています。
  3. しかし、同じユーザーが複数の投稿をしているかもしれません。つまり、複数の投稿が同じユーザーによって作成されている場合、それらの投稿の post_account_id は同じ値を持つことがあります。
    この状況で、ユーザー情報を取得する場合、次のような問題が発生します:

同じユーザーに関連する複数の投稿がある場合、そのユーザーに関するユーザー情報を複数回取得してしまう可能性があります。これは無駄なリソース使用となります。
postAccountIds リストは、この問題を解決するための手段です。以下がその動作の詳細です:

  1. 各投稿の post_account_idpostAccountIds リストに追加します。
  2. ただし、同じ post_account_id がすでにリスト内に存在する場合、重複して追加しません。つまり、リスト内にはユニークな post_account_id の値のみが含まれます。
    この結果、postAccountIds リストにはユニークな post_account_id のリストが含まれ、ユーザー情報を取得する際には重複を排除したユーザーのリストを使うことができます。これにより、同じユーザーに関連する複数の投稿がある場合でも、そのユーザーに関連するユーザー情報を一度だけ取得することができ、無駄なリソースの浪費を防ぎます

簡単に言えば、postAccountIds リストは、ユーザー情報を効率的に取得するために、同じユーザーに関連する投稿の post_account_id を重複しないように管理する役割を果たしています。

data の中身です。forEachなので投稿の数を繰り返しで、保存されているteamPostsの投稿内容が入ってきます

ステップ6: FutureBuilder ウィジェットの作成

この部分では、FutureBuilder を使用してユーザー情報を取得し、それに基づいてUIを構築します。具体的なステップを見ていきましょう。

  return FutureBuilder<Map<String, Account>?>(
  future: UserFirestore.getPostUserMap(postAccountIds),
  builder: (context, userSnapshot) {
    // この部分でユーザー情報の取得が完了しているかどうかをチェックし、UIを構築します。
  },
);

  • FutureBuilder は、非同期操作を実行し、その結果に応じてUIを構築するためのウィジェットです。ここでは、ユーザー情報を取得する非同期操作を行います。

  • future: UserFirestore.getPostUserMap(postAccountIds) では、UserFirestore クラス内の getPostUserMap メソッドを呼び出し、postAccountIds リストを渡しています。このメソッドは、postAccountIds に含まれるユーザーIDに対応するユーザー情報を非同期に取得する役割を担っています。

getPostUserMapの中身
static Future<Map<String, Account>?> getPostUserMap(
      List<String> accountIds) async {
    Map<String, Account> map = {};
    try {
      await Future.forEach(accountIds, (accountId) async {
        var doc = await users.doc(accountId).get();
        Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
        Account postAccount = Account(
          name: data["name"],
          id: accountId,
          imagePath: data["image_path"],
          profile: data["user_profile"],
        );
        print("投稿ユーザー情報取得: ${postAccount.id}");
        map[accountId] = postAccount;
      });
      print("投稿ユーザー情報取得完了");
      return map;
    } on FirebaseException catch (e) {
      print("投稿ユーザー情報取得失敗");
      return null;
    }
  }

与えられたユーザーID(accountIds)のリストをもとに、Firestore データベースから対応するユーザー情報を取得し、それらの情報をマップ形式で返す非同期関数です.
postAccountの中身は

mapの中身はkeyにアカウントidが入り、valueにはアカウントの情報が入っている

  • builder コールバック関数は、非同期操作の結果に応じてUIを構築します。具体的には、userSnapshot パラメータがユーザー情報の取得状態を示します。

ステップ7: ユーザー情報の取得が完了している場合の処理

if (userSnapshot.hasData &&
    userSnapshot.connectionState == ConnectionState.done) {
  // ユーザー情報の取得が完了している場合、ListView.builderで投稿データを表示します。
  return ListView.builder(
    itemCount: postSnapshot.data!.docs.length,
    itemBuilder: (BuildContext context, int index) {
      // 各投稿のデータを取得してTeamPostWidgetを表示します。
      Map<String, dynamic> data =
          postSnapshot.data!.docs[index].data() as Map<String, dynamic>;
      return TeamPostWidget(
        data: data,
        id: postSnapshot.data!.docs[index].id,
      );
    },
  );
}

  • if (userSnapshot.hasData && userSnapshot.connectionState == ConnectionState.done) は、ユーザー情報が取得済みかつデータの読み込みが完了している場合に対応します。つまり、ユーザー情報が利用可能で、データの読み込みが完了している状態です。この場合、投稿データを表示するための ListView.builder を返します。

  • itemCount は取得した投稿データ(TeamPosts)の数を表しています
    どうしてdataがpostSnapshot.data!.docs.lengthではなくpostSnapshot.data!.docs[index].dataなのかというと

postSnapshot.data!.docs.length はリスト内のアイテムの総数を表しており、

個別のアイテムごとにデータを取得するためには各アイテムの index を使用する必要があります。そのため、itemBuilder 内では index を使用して適切な位置のデータを取得しています。

TeamPostWidgetの中身
  import 'package:flutter/material.dart';

import '../models/model/game_model.dart';
import '../models/model/team_model.dart';
import '../screen/post/post_item_widget.dart';

class TeamPostWidget extends StatelessWidget {
  final Map<String, dynamic> data;
  final String id; // id を追加

  TeamPostWidget({required this.data, required this.id});

  
  Widget build(BuildContext context) {
    List<String> locationList = List<String>.from(data["location"]);
    List<String> targetList = List<String>.from(data["target"]);
    List<String> ageList = List<String>.from(data["age"]);
    List<String> prefectureAndLocation = [...locationList, data["prefecture"]];

    TeamPost post = TeamPost(
        id: id,
        postAccountId: data["post_account_id"],
        locationTagList: locationList,
        prefecture: data["prefecture"],
        goal: data["goal"],
        activityTime: data["activityTime"],
        memberCount: data["memberCount"],
        teamName: data["teamName"],
        targetList: targetList,
        note: data["note"],
        imageUrl: data["imageUrl"],
        createdTime: data["created_time"],
        searchCriteria: data["searchCriteria"],
        ageList: ageList,
        cost: data["cost"],
        headerUrl: data["headerUrl"],
        prefectureAndLocation: prefectureAndLocation);
    print(post);
    return TeamListItemWidget(
      recruitment: data,
      postId: post.id,
    ); // listItemウィジェットを返す
  }
}
  • コンストラクタ TeamPostWidget は、2つのパラメータ dataid を受け取ります。これらのパラメータはウィジェットが構築される際に渡されるデータです。

  • build メソッドは、ウィジェットが実際に描画される内容を定義します。このメソッド内で、渡されたデータを元に TeamPost クラスのインスタンスを作成し、それを使用して TeamListItemWidget を作成しています。

  • TeamPost クラスのインスタンスを作成する際、コンストラクタにさまざまなデータフィールドを渡しています。これらのフィールドには idpost_account_id、locationTagListprefecturegoal などが含まれます。

  • TeamListItemWidget を作成する際、recruitmentpostId パラメータを渡しています。recruitmentdata パラメータそのもので、postIdTeamPost インスタンスの id フィールドから取得されています。

  • 簡単に言えば、TeamPostWidget は特定のデータを受け取り、それを元に TeamPost インスタンスを作成し、そのデータを表示するために TeamListItemWidget を作成します。このウィジェットは、Flutterアプリケーションの特定の部分に組み込まれ、データを視覚的に表示する役割を果たします。

TeamListItemWidgetの中身(listのui)
import 'package:basketball_app/screen/post/post_detail_page.dart';
import 'package:basketball_app/widgets/post_list_widgets.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import '../../widgets/account_circle.dart';

//チーム募集 TeamListItemWidget
class TeamListItemWidget extends StatefulWidget {
  final Map<String, dynamic> recruitment;
  final String postId;

  TeamListItemWidget({
    required this.recruitment,
    required this.postId,
  });

  
  State<TeamListItemWidget> createState() => _TeamListItemWidgetState();
}

class _TeamListItemWidgetState extends State<TeamListItemWidget> {
  
  void initState() {
    // TODO: implement initState
    super.initState();
    print(widget.recruitment["created_time"].toDate());
  }

  
  Widget build(BuildContext context) {
    DateTime createAtDateTime = widget.recruitment["created_time"].toDate();
    String formattedCreatedAt =
        DateFormat('yyyy/MM/dd').format(createAtDateTime);

    List<String> targetList = List<String>.from(widget.recruitment["target"]);
    List<String> ageList = List<String>.from(widget.recruitment["age"]);
    List<String> locationList =
        List<String>.from(widget.recruitment["location"]);

    return Stack(
      children: [
        // 背景画像を配置したい場合
        Container(
          margin: const EdgeInsets.all(20),
          // padding: EdgeInsets.only(right: 13),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(10),
            color: Colors.white,
            boxShadow: [
              BoxShadow(
                color: Colors.black26,
                blurRadius: 5,
                offset: Offset(0, 3),
              ),
            ],
          ),
          child: Stack(
            children: [
              ClipRRect(
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(10),
                    topRight: Radius.circular(10),
                  ),
                  child: ColorFiltered(
                    colorFilter: ColorFilter.mode(
                      Colors.black.withOpacity(0.5), // 透明度を調整して画像を暗くします
                      BlendMode.srcATop,
                    ),
                    child: widget.recruitment["headerUrl"] != null
                        ? Image.network(
                            widget.recruitment["headerUrl"]!,
                            width: double.infinity,
                            height: 125,
                            fit: BoxFit.cover,
                          )
                        : Image.asset(
                            'assets/images/headerImage.jpg',
                            width: double.infinity,
                            height: 125,
                            fit: BoxFit.cover,
                          ),
                  )),
              Padding(
                padding: EdgeInsets.only(right: 10.0),
                child: Column(
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(10),
                          ),
                          child: Container(
                            height: 40,
                            width: 60,
                            child: Padding(
                              padding: const EdgeInsets.all(8.0),
                              child: Text(
                                widget.recruitment["prefecture"],
                                style: TextStyle(
                                  color: Colors.white,
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                            ),
                            decoration: BoxDecoration(
                              color: Colors.indigo,
                            ),
                          ),
                        ),
                        Row(
                          children: [
                            Text(
                              "更新日:$formattedCreatedAt",
                              style: TextStyle(
                                  color: Colors.white,
                                  fontWeight: FontWeight.bold),
                            ),
                          ],
                        ),
                      ],
                    ),
                    Padding(
                      padding: const EdgeInsets.only(left: 16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(left: 15.0, top: 15),
                            child: Row(
                              children: [
                                widget.recruitment["imageUrl"] != null
                                    ? ImageCircle(
                                        imagePath:
                                            widget.recruitment["imageUrl"])
                                    : NoImageCircle(),
                              ],
                            ),
                          ),
                          Column(
                            children: [
                              Text(
                                widget.recruitment["teamName"],
                                style: TextStyle(
                                    fontWeight: FontWeight.bold,
                                    fontSize: 20,
                                    color: Colors.black),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                              SizedBox(
                                height: 20,
                              ),
                              Row(
                                children: [
                                  Text(
                                    widget.recruitment["searchCriteria"],
                                    style: TextStyle(
                                        fontWeight: FontWeight.w500,
                                        fontSize: 16),
                                  ),
                                ],
                              ),
                            ],
                          ),
                          SizedBox(height: 10),
                          Container(
                            decoration: BoxDecoration(
                              border: Border(
                                bottom: BorderSide(
                                    color: Colors.black12, width: 1.0), // 下線
                              ),
                            ),
                          ),
                          SizedBox(height: 8),
                          PostIconWithText(
                              icon: Icons.location_on,
                              text: locationList.join(", "),
                              color: Colors.indigo),
                          SizedBox(height: 8),
                          Container(
                            decoration: BoxDecoration(
                              border: Border(
                                bottom: BorderSide(
                                    color: Colors.black12, width: 1.0), // 下線
                              ),
                            ),
                          ),
                          SizedBox(height: 8),
                          PostIconWithText(
                              icon: Icons.calendar_month,
                              text: widget.recruitment["activityTime"],
                              color: Colors.black),
                          SizedBox(height: 8),
                          Container(
                            decoration: BoxDecoration(
                              border: Border(
                                bottom: BorderSide(
                                    color: Colors.black12, width: 1.0), // 下線
                              ),
                            ),
                          ),
                          SizedBox(height: 8),
                          PostIconWithText(
                              icon: Icons.group_add,
                              text: ageList.join(', '),
                              color: Colors.black),
                          SizedBox(height: 8),
                          Container(
                            decoration: BoxDecoration(
                              border: Border(
                                bottom: BorderSide(
                                    color: Colors.black12, width: 1.0), // 下線
                              ),
                            ),
                          ),
                          SizedBox(height: 8),
                          PostIconWithList(
                              icon: Icons.groups, list: targetList),
                          SizedBox(height: 8),
                          Container(
                            decoration: BoxDecoration(
                              border: Border(
                                bottom: BorderSide(
                                    color: Colors.black12, width: 1.0), // 下線
                              ),
                            ),
                          ),
                          Center(
                            child: TextButton(
                              onPressed: () {
                                print(widget.postId);
                                Navigator.push(
                                  context,
                                  MaterialPageRoute(
                                    builder: (context) => TeamPostDetailPage(
                                      postId: widget.postId,
                                    ),
                                  ),
                                );
                              },
                              child: Text(
                                "詳細を見る",
                                style: TextStyle(
                                    fontSize: 14, fontWeight: FontWeight.bold),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                    // SizedBox(
                    //   height: 20,
                    // ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

}

ステップ8: ユーザー情報の取得中の処理

else {
  // ユーザー情報の取得中はローディングアイコンを表示します。
  return Center(
    child: CircularProgressIndicator(),
  );
}
  • else ブロックは、ユーザー情報の取得中またはエラーが発生した場合に対応します。この場合、ローディング中の表示(くるくる回るアイコン)を返します。

  • これにより、Firestoreから投稿データをリアルタイムに取得し、各投稿に関連するユーザー情報を効率的に取得してUIに表示するプロセスが完了します。ユーザーは最新の投稿をリアルタイムで見ることができ、アプリのユーザーエクスペリエンスが向上します。

結果画面

表示させることができました!!

最後に感想

記事を最後までお読みいただき、ありがとうございました!
この記事が少しでも役立つ情報を提供できたなら、とても嬉しく思います!

私自身もまだ学び続けており、100%理解しているわけではありません。したがって、記事内に誤った情報や誤解を招く要素があるかもしれません。その点については、遠慮せずにご指摘いただければ幸いです。皆さんからのフィードバックは、記事をより良くするための貴重な手助けとなります。

今後もFirebase FirestoreやFlutterに関する情報を共有し、共に成長していければと思っています。どうもありがとうございました!

参考にさせていただいたFlutterラボ様のリンクです
https://www.udemy.com/course/flutter-firebase-sns/

Discussion