【メンバー追加】メールアドレスから登録済みユーザーを検索して追加する
このページでやること
このページでは、メールアドレスを入力して、登録済みユーザーをチームメンバーに追加します。
ユーザー情報は、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にする
メンバー追加では、メールアドレス入力欄を使います。
そのため、MemberListPage を StatefulWidget にします。
今の 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
今回、追加された人の role は member にします。
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:追加された人の画面で確認する
追加された人でログインします。
ログアウト
↓
追加された人でログイン
トーク一覧に、追加されたチームが表示されれば成功です。
追加されたチームが見える
これは、memberIds に uid が追加されているためです。
よくあるエラーと直し方
| エラー | 原因 | 直し方 |
|---|---|---|
showAddMemberSheet isn't defined | 関数がない | showAddMemberSheet() を追加 |
addMemberByEmail isn't defined | 追加処理がない | addMemberByEmail() を追加 |
emailController isn't defined | Controllerがない | emailController を追加 |
widget.teamId が使えない | StatefulWidgetになっていない | MemberListPage をStatefulWidgetにする |
| ユーザーが見つからない | usersに登録されていない | 先に新規登録する |
| 追加したのにチーム一覧に出ない | memberIdsにuidがない | arrayUnion([uid]) を確認 |
permission-denied | Firestore 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や環境変数は不要。
次のページでやること
次のページでは、メンバーの権限を変更できるようにします。
member を admin にしたり、viewer に変更したりできる画面を作ります。
