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

【メンバー追加】メールアドレスから登録済みユーザーを検索して追加する

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

このページでやること

このページでは、メールアドレスを入力して、登録済みユーザーをチームメンバーに追加します。

ユーザー情報は、Firestoreのここに保存されています。

users/{uid}

チームメンバー情報は、ここに保存します。

teams/{teamId}/members/{uid}

つまり、流れはこうです。

メールアドレスを入力
↓
usersから登録済みユーザーを探す
↓
見つかったらmembersに追加
↓
memberIdsにもuidを追加
↓
メンバー一覧に表示

今日のゴール

メンバー一覧画面の右下 + ボタンから、メンバーを追加できるようにします。

メンバー一覧画面
↓
右下の+を押す
↓
メールアドレスを入力
↓
追加する
↓
チームメンバーに追加される

最初は、追加した人の権限を member にします。


npmや環境変数は必要?

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

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

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

必要なimportはこちらです。

import 'package:cloud_firestore/cloud_firestore.dart';

Step 1:main.dartを開く

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

lib/main.dart

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

code lib/main.dart

Step 2:MemberListPageをStatefulWidgetにする

メンバー追加では、メールアドレス入力欄を使います。

そのため、MemberListPageStatefulWidget にします。

今の MemberListPage を、次の形に変更します。

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

  final String teamId;
  final String teamName;

  @override
  State<MemberListPage> createState() => _MemberListPageState();
}

class _MemberListPageState extends State<MemberListPage> {
  final emailController = TextEditingController();

  bool isAdding = false;
  String? errorText;

  @override
  void dispose() {
    emailController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppColors.bg,
      appBar: AppBar(
        title: Text('${widget.teamName}のメンバー'),
      ),
      body: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
        stream: FirebaseFirestore.instance
            .collection('teams')
            .doc(widget.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,
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: showAddMemberSheet,
        child: const Icon(Icons.add),
      ),
    );
  }
}

まだ showAddMemberSheet は作っていないので、次で追加します。


Step 3:メンバー追加ボトムシートを作る

_MemberListPageState の中に、次の関数を追加します。

Future<void> showAddMemberSheet() async {
  emailController.clear();
  errorText = null;

  await showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true,
    backgroundColor: Colors.white,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(
        top: Radius.circular(20),
      ),
    ),
    builder: (context) {
      return StatefulBuilder(
        builder: (context, setSheetState) {
          return Padding(
            padding: EdgeInsets.only(
              left: 20,
              right: 20,
              top: 20,
              bottom: MediaQuery.of(context).viewInsets.bottom + 20,
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text(
                  'メンバーを追加',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.w900,
                    color: AppColors.text,
                  ),
                ),
                const SizedBox(height: 12),
                const Text(
                  '登録済みユーザーのメールアドレスを入力してください。',
                  style: TextStyle(
                    color: AppColors.subText,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const SizedBox(height: 12),
                AppTextField(
                  controller: emailController,
                  label: 'メールアドレス',
                  keyboardType: TextInputType.emailAddress,
                ),
                if (errorText != null) ...[
                  const SizedBox(height: 12),
                  ErrorBox(message: errorText!),
                ],
                const SizedBox(height: 16),
                FilledButton(
                  onPressed: isAdding
                      ? null
                      : () async {
                          await addMemberByEmail(setSheetState);
                        },
                  child: Text(isAdding ? '追加中...' : '追加する'),
                ),
              ],
            ),
          );
        },
      );
    },
  );
}

これで、右下の + からメール入力画面を出せます。


Step 4:メールアドレスからユーザーを探す

ユーザー登録時に、users/{uid} にプロフィールを保存しました。

そこからメールアドレスで検索します。

Firestoreの保存イメージです。

users/{uid}
  displayName: 山田 太郎
  email: yamada@example.com
  department: 開発部

メールアドレスで探すには、次のようにします。

final userQuery = await FirebaseFirestore.instance
    .collection('users')
    .where('email', isEqualTo: email)
    .limit(1)
    .get();

意味はこうです。

usersを見る
↓
emailが入力値と同じ人を探す
↓
1件だけ取得する

Step 5:メンバー追加処理を作る

_MemberListPageState の中に、次の関数を追加します。

Future<void> addMemberByEmail(StateSetter setSheetState) async {
  final email = emailController.text.trim();

  if (email.isEmpty) {
    setSheetState(() {
      errorText = 'メールアドレスを入力してください。';
    });
    return;
  }

  setSheetState(() {
    isAdding = true;
    errorText = null;
  });

  try {
    final userQuery = await FirebaseFirestore.instance
        .collection('users')
        .where('email', isEqualTo: email)
        .limit(1)
        .get();

    if (userQuery.docs.isEmpty) {
      setSheetState(() {
        errorText = 'このメールアドレスのユーザーは見つかりません。';
      });
      return;
    }

    final userDoc = userQuery.docs.first;
    final userData = userDoc.data();

    final uid = userDoc.id;
    final displayName =
        (userData['displayName'] ?? '名前未設定').toString();
    final userEmail = (userData['email'] ?? email).toString();

    final memberRef = FirebaseFirestore.instance
        .collection('teams')
        .doc(widget.teamId)
        .collection('members')
        .doc(uid);

    final memberDoc = await memberRef.get();

    if (memberDoc.exists) {
      setSheetState(() {
        errorText = 'このユーザーはすでにメンバーです。';
      });
      return;
    }

    await memberRef.set({
      'uid': uid,
      'email': userEmail,
      'displayName': displayName,
      'role': 'member',
      'joinedAt': FieldValue.serverTimestamp(),
    });

    await FirebaseFirestore.instance
        .collection('teams')
        .doc(widget.teamId)
        .update({
      'memberIds': FieldValue.arrayUnion([uid]),
      'updatedAt': FieldValue.serverTimestamp(),
    });

    if (mounted) {
      Navigator.of(context).pop();
    }
  } on FirebaseException catch (e) {
    setSheetState(() {
      errorText = e.message ?? 'メンバー追加に失敗しました。';
    });
  } catch (_) {
    setSheetState(() {
      errorText = 'メンバー追加に失敗しました。';
    });
  } finally {
    if (mounted) {
      setSheetState(() {
        isAdding = false;
      });
    }
  }
}

これで、メールアドレスから登録済みユーザーを検索して、チームに追加できます。


Step 6:保存する場所を確認する

追加したメンバーは、ここに保存します。

teams/{teamId}/members/{uid}

保存する内容はこちらです。

uid
email
displayName
role
joinedAt

今回、追加された人の rolemember にします。

role: member

Step 7:memberIdsにも追加する

この部分で、チーム本体の memberIds にも追加しています。

await FirebaseFirestore.instance
    .collection('teams')
    .doc(widget.teamId)
    .update({
  'memberIds': FieldValue.arrayUnion([uid]),
  'updatedAt': FieldValue.serverTimestamp(),
});

memberIds は、チーム一覧を取得するときに使っています。

.where('memberIds', arrayContains: user.uid)

そのため、新しいメンバーを追加したら、memberIds にも uid を入れる必要があります。


Step 8:arrayUnionとは何か

arrayUnion は、配列に値を追加する命令です。

FieldValue.arrayUnion([uid])

同じ uid がすでに入っている場合は、重複して追加されません。

memberIds: [A, B]
↓
arrayUnion([C])
↓
memberIds: [A, B, C]

同じ人をもう一度追加しても、増えません。

memberIds: [A, B]
↓
arrayUnion([B])
↓
memberIds: [A, B]

Step 9:すでにメンバーの場合は止める

この部分で、同じ人を二重に追加しないようにしています。

final memberDoc = await memberRef.get();

if (memberDoc.exists) {
  setSheetState(() {
    errorText = 'このユーザーはすでにメンバーです。';
  });
  return;
}

これで、同じメールアドレスを何度入力しても、重複追加を防げます。


Step 10:登録済みユーザーだけ追加する

この部分で、users に存在しないメールアドレスは追加しないようにしています。

if (userQuery.docs.isEmpty) {
  setSheetState(() {
    errorText = 'このメールアドレスのユーザーは見つかりません。';
  });
  return;
}

つまり、先にその人がアプリで新規登録している必要があります。

登録済みユーザー → 追加できる
未登録ユーザー → 追加できない

Step 11:保存する

main.dart を保存します。

Macの場合:

command + S

Windowsの場合:

Ctrl + S

Step 12:実行する

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

flutter run

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

r

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

R

Step 13:追加するユーザーを先に作る

メンバー追加を試すには、追加したい人が先にアカウント登録済みである必要があります。

アプリで新規登録します。

ログアウト
↓
新規登録
↓
別のメールアドレスで登録

例です。

名前:佐藤 花子
所属:営業部
メール:sato@example.com
パスワード:password123

登録すると、Firestoreの users に保存されます。

users/{uid}
  displayName: 佐藤 花子
  email: sato@example.com

Step 14:メンバーを追加してみる

チーム作成者でログインします。

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

追加したい人のメールアドレスを入力します。

sato@example.com

「追加する」を押します。

メンバー一覧に表示されれば成功です。

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

Step 15:Firestoreで確認する

Firebase Consoleを開きます。

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

次の場所を確認します。

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

追加したユーザーの uid があればOKです。

中身は、このようになります。

uid: 追加した人のuid
email: sato@example.com
displayName: 佐藤 花子
role: member
joinedAt: 参加日時

次に、チーム本体も確認します。

teams
↓
作成したteamId

memberIds に追加した人の uid が入っていればOKです。


Step 16:追加された人の画面で確認する

追加された人でログインします。

ログアウト
↓
追加された人でログイン

トーク一覧に、追加されたチームが表示されれば成功です。

追加されたチームが見える

これは、memberIdsuid が追加されているためです。


よくあるエラーと直し方

エラー原因直し方
showAddMemberSheet isn't defined関数がないshowAddMemberSheet() を追加
addMemberByEmail isn't defined追加処理がないaddMemberByEmail() を追加
emailController isn't definedControllerがないemailController を追加
widget.teamId が使えないStatefulWidgetになっていないMemberListPage をStatefulWidgetにする
ユーザーが見つからないusersに登録されていない先に新規登録する
追加したのにチーム一覧に出ないmemberIdsにuidがないarrayUnion([uid]) を確認
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;
    }
  }
}

これは学習用です。

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

本番では、owner または admin だけがメンバー追加できるようにします。


注意:本番では権限チェックが必要

このページでは、まず「追加できる」動きを作ります。

本番では、誰でもメンバー追加できる状態にしてはいけません。

後で、次のように制限します。

owner → 追加できる
admin → 追加できる
member → 追加できない
viewer → 追加できない

Flutter側とFirestore Rules側の両方で制限します。


最短作業まとめ

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

1. メール入力用Controllerを作る

final emailController = TextEditingController();

2. 右下の+につなげる

floatingActionButton: FloatingActionButton(
  onPressed: showAddMemberSheet,
  child: const Icon(Icons.add),
),

3. usersからメール検索する

final userQuery = await FirebaseFirestore.instance
    .collection('users')
    .where('email', isEqualTo: email)
    .limit(1)
    .get();

4. membersに追加する

await memberRef.set({
  'uid': uid,
  'email': userEmail,
  'displayName': displayName,
  'role': 'member',
  'joinedAt': FieldValue.serverTimestamp(),
});

5. memberIdsにも追加する

await FirebaseFirestore.instance
    .collection('teams')
    .doc(widget.teamId)
    .update({
  'memberIds': FieldValue.arrayUnion([uid]),
  'updatedAt': FieldValue.serverTimestamp(),
});

チェックリスト

□ main.dartを開いた
□ MemberListPageをStatefulWidgetにした
□ emailControllerを作った
□ showAddMemberSheet()を作った
□ addMemberByEmail()を作った
□ メール未入力チェックをした
□ usersからemailで検索した
□ ユーザーが見つからない時のエラーを出した
□ すでにメンバーの場合のエラーを出した
□ teams/{teamId}/members/{uid} に保存した
□ roleをmemberで保存した
□ memberIdsにuidを追加した
□ 保存した
□ flutter runで確認した
□ メンバー一覧に追加ユーザーが表示された
□ 追加されたユーザー側でチームが表示された

ミニ確認問題

Q1. メールアドレスからユーザーを探す場所はどこですか?

回答

次の場所です。

users

email が一致するユーザーを探します。


Q2. 追加したメンバーはどこに保存しますか?

回答

次の場所に保存します。

teams/{teamId}/members/{uid}

Q3. 追加した人の初期roleは何ですか?

回答

member です。


Q4. memberIdsにもuidを追加する理由は何ですか?

回答

追加された人のトーク一覧に、そのチームを表示するためです。

チーム一覧では、memberIds に自分の uid が入っているチームだけを取得しています。


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

回答

必要ありません。

Firestoreの既存データを使って進めます。


このページのまとめ

  • メンバー追加は、メールアドレスから登録済みユーザーを検索して行う。
  • 検索場所は users
  • 追加先は teams/{teamId}/members/{uid}
  • 初期権限は member にする。
  • memberIds にも uid を追加する。
  • memberIds に追加しないと、追加された人のチーム一覧に表示されない。
  • 未登録ユーザーは追加できない。
  • 本番では owner / admin だけが追加できるようにする。
  • このページではnpmや環境変数は不要。

次のページでやること

次のページでは、メンバーの権限を変更できるようにします。

memberadmin にしたり、viewer に変更したりできる画面を作ります。

教材トップへ戻る