Flutterアプリケーション開発概論

【メンバー管理画面】チームの参加メンバーを一覧表示する

37LINE風チームタスク管理アプリを作りながら、ログイン・データベース・権限管理を学ぶ
FlutteriOSAndroidMacOSWindows基礎から学ぶ開発アプリ開発

このページでやること

このページでは、チームに参加しているメンバーを一覧表示します。

メンバー情報は、Firestoreのここに保存されています。

teams/{teamId}/members/{uid}

このデータを取得して、画面に表示します。

チーム詳細画面
↓
メンバー管理画面を開く
↓
参加メンバーを一覧表示

今日のゴール

チームのメンバーを、次のように表示できるようにします。

山田 太郎
yamada@example.com
オーナー

佐藤 花子
sato@example.com
メンバー

このページでは、追加・削除・権限変更はまだ行いません。

まずは、参加メンバーを表示するだけです。


npmや環境変数は必要?

このページでは、npmは使いません。

環境変数も設定しません。

すでに使っているFirestoreだけで進めます。

必要なimportはこちらです。

import 'package:cloud_firestore/cloud_firestore.dart';

Step 1:main.dartを開く

次のファイルを開きます。

lib/main.dart

ターミナルから開く場合はこちらです。

code lib/main.dart

Step 2:メンバーの保存場所を確認する

チームメンバーは、Firestoreのここに保存されています。

teams/{teamId}/members/{uid}

例です。

teams/team001/members/user001
  uid: user001
  email: yamada@example.com
  displayName: 山田 太郎
  role: owner
  joinedAt: 参加日時

この中で、画面に出すのは主にこの3つです。

displayName
email
role

Step 3:TaskListPageにメンバー画面へのボタンを追加する

まず、タスク一覧画面の右上から、メンバー管理画面を開けるようにします。

TaskListPageAppBar を探します。

appBar: AppBar(
  title: Text(widget.teamName),
),

これを、次のように変更します。

appBar: AppBar(
  title: Text(widget.teamName),
  actions: [
    IconButton(
      tooltip: 'メンバー',
      onPressed: () {
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (_) => MemberListPage(
              teamId: widget.teamId,
              teamName: widget.teamName,
            ),
          ),
        );
      },
      icon: const Icon(Icons.group),
    ),
  ],
),

これで、右上の人型アイコンからメンバー一覧画面を開けます。


Step 4:MemberListPageを作る

TaskListPage の下あたりに、次のコードを追加します。

class MemberListPage extends StatelessWidget {
  const MemberListPage({
    super.key,
    required this.teamId,
    required this.teamName,
  });

  final String teamId;
  final String teamName;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppColors.bg,
      appBar: AppBar(
        title: Text('$teamNameのメンバー'),
      ),
      body: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
        stream: FirebaseFirestore.instance
            .collection('teams')
            .doc(teamId)
            .collection('members')
            .snapshots(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const LoadingPage(message: 'メンバーを読み込んでいます...');
          }

          if (snapshot.hasError) {
            return Center(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: ErrorBox(
                  message: 'メンバー一覧の取得に失敗しました。',
                ),
              ),
            );
          }

          final members = snapshot.data?.docs ?? [];

          if (members.isEmpty) {
            return const Center(
              child: Text(
                'まだメンバーがいません。',
                style: TextStyle(
                  color: AppColors.subText,
                  fontWeight: FontWeight.w700,
                ),
              ),
            );
          }

          return ListView.separated(
            padding: const EdgeInsets.all(16),
            itemCount: members.length,
            separatorBuilder: (_, __) => const SizedBox(height: 10),
            itemBuilder: (context, index) {
              final doc = members[index];
              final data = doc.data();

              final displayName =
                  (data['displayName'] ?? '名前未設定').toString();
              final email = (data['email'] ?? '').toString();
              final roleText = (data['role'] ?? 'member').toString();
              final role = teamRoleFromString(roleText);

              return MemberCard(
                displayName: displayName,
                email: email,
                role: role,
              );
            },
          );
        },
      ),
    );
  }
}

これで、Firestoreからメンバー一覧を取得できます。


Step 5:Firestoreからmembersを取得する部分を見る

今回の中心はここです。

FirebaseFirestore.instance
    .collection('teams')
    .doc(teamId)
    .collection('members')
    .snapshots()

意味はこうです。

teamsを見る
↓
選んだteamIdを見る
↓
その中のmembersを見る
↓
変更があれば自動で画面更新

取得場所はここです。

teams/{teamId}/members

Step 6:MemberCardを作る

次に、メンバー1人分を表示する MemberCard を作ります。

MemberListPage の下に、次のコードを追加します。

class MemberCard extends StatelessWidget {
  const MemberCard({
    super.key,
    required this.displayName,
    required this.email,
    required this.role,
  });

  final String displayName;
  final String email;
  final TeamRole role;

  @override
  Widget build(BuildContext context) {
    return AppCard(
      child: Row(
        children: [
          CircleAvatar(
            backgroundColor: AppColors.lineGreen,
            child: Text(
              displayName.isNotEmpty ? displayName.substring(0, 1) : '?',
              style: const TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.w900,
              ),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  displayName,
                  style: const TextStyle(
                    color: AppColors.text,
                    fontSize: 16,
                    fontWeight: FontWeight.w900,
                  ),
                ),
                if (email.isNotEmpty) ...[
                  const SizedBox(height: 4),
                  Text(
                    email,
                    style: const TextStyle(
                      color: AppColors.subText,
                      fontSize: 13,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ],
              ],
            ),
          ),
          _Label(text: teamRoleLabel(role)),
        ],
      ),
    );
  }
}

MemberCard は、メンバー1人分の表示部品です。


Step 7:TeamRoleを確認する

前のページで作った TeamRole があるか確認します。

enum TeamRole {
  owner,
  admin,
  member,
  viewer,
}

Firestoreの role は文字列です。

owner
admin
member
viewer

Dart側では、これを TeamRole に変換して扱います。


Step 8:teamRoleFromStringを確認する

次の関数があるか確認します。

TeamRole teamRoleFromString(String? value) {
  switch (value) {
    case 'owner':
      return TeamRole.owner;
    case 'admin':
      return TeamRole.admin;
    case 'viewer':
      return TeamRole.viewer;
    case 'member':
    default:
      return TeamRole.member;
  }
}

これは、Firestoreの文字列をDartの権限に変換する関数です。

owner → TeamRole.owner
admin → TeamRole.admin
member → TeamRole.member
viewer → TeamRole.viewer

Step 9:teamRoleLabelを確認する

画面では、日本語で表示します。

次の関数があるか確認します。

String teamRoleLabel(TeamRole role) {
  switch (role) {
    case TeamRole.owner:
      return 'オーナー';
    case TeamRole.admin:
      return '管理者';
    case TeamRole.member:
      return 'メンバー';
    case TeamRole.viewer:
      return '閲覧者';
  }
}

これで、画面表示はこうなります。

TeamRole.owner  → オーナー
TeamRole.admin  → 管理者
TeamRole.member → メンバー
TeamRole.viewer → 閲覧者

Step 10:右上のアイコンから開く流れを確認する

今回の画面遷移は、こうです。

TaskListPage
↓
右上のメンバーアイコンを押す
↓
MemberListPageを開く
↓
teamIdを使ってmembersを取得

teamId を渡しているので、そのチームだけのメンバーを表示できます。

MemberListPage(
  teamId: widget.teamId,
  teamName: widget.teamName,
)

Step 11:保存する

main.dart を保存します。

Macの場合:

command + S

Windowsの場合:

Ctrl + S

Step 12:実行する

ターミナルで実行します。

flutter run

すでに起動している場合は、ターミナルで r を押します。

r

うまく反映されない場合は、R を押します。

R

Step 13:画面を確認する

アプリを開きます。

ログイン
↓
トーク一覧
↓
チームをタップ
↓
右上のメンバーアイコンを押す

メンバー一覧画面が開きます。

チーム作成者が表示されていれば成功です。

自分の名前
自分のメールアドレス
オーナー

Step 14:Firestoreで確認する

Firebase Consoleを開きます。

https://console.firebase.google.com/

次の場所を確認します。

Firestore Database
↓
teams
↓
作成したteamId
↓
members

ここに、自分の uid のドキュメントがあるはずです。

中身は、このような形です。

uid: 自分のuid
email: 自分のメールアドレス
displayName: 自分の名前
role: owner
joinedAt: 参加日時

Step 15:テスト用メンバーを手動で追加する

まだメンバー追加機能がないので、テストしたい場合はFirebase Consoleで手動追加します。

members の中に新しいドキュメントを作ります。

ドキュメントIDは仮でOKです。

test-user-001

フィールドは次のように入れます。

uid: test-user-001
email: test@example.com
displayName: テスト太郎
role: member
joinedAt: 現在時刻

アプリに戻って、テスト太郎が表示されればOKです。


よくあるエラーと直し方

エラー原因直し方
MemberListPage isn't defined画面を作っていないMemberListPage を追加
MemberCard isn't definedカードを作っていないMemberCard を追加
teamRoleFromString isn't defined変換関数がない前ページの関数を追加
teamRoleLabel isn't defined日本語表示関数がないteamRoleLabel() を追加
_Label isn't definedラベル部品がない_Label を追加
メンバーが表示されないmembersにデータがないFirestoreのmembersを確認
permission-deniedFirestore Rulesで拒否開発用Rulesを確認

permission-denied が出たとき

開発中だけ、Firestore Rulesを次のようにして確認できます。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

これは学習用です。

本番公開では使わないでください。

本番では、チームメンバーだけがメンバー一覧を見られるようにします。


文字の1文字目でエラーが出る場合

この部分でエラーが出ることがあります。

displayName.substring(0, 1)

displayName が空の場合は ? を出すようにしています。

displayName.isNotEmpty ? displayName.substring(0, 1) : '?'

日本語や絵文字をより安全に扱うなら、次のようにしてもOKです。

displayName.isNotEmpty ? displayName.characters.first : '?'

ただし、characters でエラーが出る場合は、substring(0, 1) のままで進めてください。


最短作業まとめ

読むのが大変な人は、ここだけ見てください。

1. AppBarにメンバーボタンを追加

actions: [
  IconButton(
    tooltip: 'メンバー',
    onPressed: () {
      Navigator.of(context).push(
        MaterialPageRoute(
          builder: (_) => MemberListPage(
            teamId: widget.teamId,
            teamName: widget.teamName,
          ),
        ),
      );
    },
    icon: const Icon(Icons.group),
  ),
],

2. membersを取得する

stream: FirebaseFirestore.instance
    .collection('teams')
    .doc(teamId)
    .collection('members')
    .snapshots(),

3. メンバーを表示する

return MemberCard(
  displayName: displayName,
  email: email,
  role: role,
);

4. 保存して実行

flutter run

チェックリスト

□ main.dartを開いた
□ TaskListPageのAppBarにメンバーアイコンを追加した
□ MemberListPageを作った
□ teamIdを受け取れるようにした
□ teamNameを受け取れるようにした
□ teams/{teamId}/members を取得した
□ displayNameを取り出した
□ emailを取り出した
□ roleを取り出した
□ teamRoleFromStringでroleを変換した
□ MemberCardを作った
□ roleを日本語で表示した
□ 保存した
□ flutter runで確認した
□ メンバー一覧が表示された

ミニ確認問題

Q1. メンバー一覧はFirestoreのどこから取得しますか?

回答

次の場所から取得します。

teams/{teamId}/members

Q2. メンバーの権限はどの項目に保存されていますか?

回答

role に保存されています。


Q3. Firestoreの role: owner は画面では何と表示しますか?

回答

オーナー と表示します。


Q4. このページでnpmや環境変数は必要ですか?

回答

必要ありません。

Firestoreから members を取得して表示するだけです。


このページのまとめ

  • メンバー情報は teams/{teamId}/members/{uid} に保存する。
  • メンバー一覧は teams/{teamId}/members から取得する。
  • StreamBuildersnapshots() でリアルタイムに表示する。
  • 表示する主な項目は displayNameemailrole
  • roleTeamRole に変換して扱う。
  • 画面では オーナー管理者メンバー閲覧者 と表示する。
  • このページではnpmや環境変数は不要。

次のページでやること

次のページでは、メンバーをメールアドレスで追加する画面を作ります。

教材トップへ戻る