【チーム作成】teams/{teamId} と members/{uid} を同時に作成する
このページでやること
このページでは、ログイン中のユーザーが新しいチームを作れるようにします。
チームを作るときは、Firestoreに次の2つを同時に作ります。
teams/{teamId}
teams/{teamId}/members/{uid}
短く言うと、こうです。
チーム情報を作る
↓
作った人を、そのチームのownerとして登録する
owner とは、チームの所有者です。
チームを作った人は、最初の管理者になります。
今日のゴール
チーム作成ボタンを押すと、次の流れで動くようにします。
チーム名を入力する
↓
作成ボタンを押す
↓
Firestoreにteams/{teamId}を作る
↓
Firestoreにteams/{teamId}/members/{uid}を作る
↓
チーム一覧に表示される
保存する形は、次のようになります。
teams/{teamId}
name: 開発チーム
ownerId: ログイン中ユーザーのuid
memberIds: [ログイン中ユーザーのuid]
createdAt: 作成日時
updatedAt: 更新日時
teams/{teamId}/members/{uid}
uid: ログイン中ユーザーのuid
email: メールアドレス
displayName: 表示名
role: owner
joinedAt: 参加日時
このページで出てくる単語
| 単語 | 一言説明 |
|---|---|
teamId | チームごとに作られるID |
uid | ユーザーごとに作られるID |
owner | チームを作った人。所有者 |
members | チームに参加している人の情報 |
memberIds | チーム参加者のuid一覧 |
add() | Firestoreに新しいドキュメントを作る命令 |
set() | 指定した場所にデータを保存する命令 |
FieldValue.serverTimestamp() | Firestore側の現在時刻を保存する命令 |
ドキュメントとは、Firestoreの1件分のデータです。
npmや環境変数はこのページで必要?
このページでは、npmは使いません。
環境変数も設定しません。
必要なのは、すでに追加した次の2つです。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
このページでは、main.dart にチーム作成処理を追加していきます。
Step 1:main.dartを開く
Flutterプロジェクトの中で、次のファイルを開きます。
lib/main.dart
VS Codeを使っている場合は、ターミナルで次を実行してもOKです。
code lib/main.dart
code が使えない場合は、VS Codeの左側から lib/main.dart を開いてください。
Step 2:importを確認する
main.dart の上に、次の2つがあるか確認します。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
cloud_firestore はFirestoreにデータを保存するために使います。
firebase_auth はログイン中のユーザー情報を取得するために使います。
Step 3:TeamListPageをStatefulWidgetにする
チーム作成では、入力欄やエラー表示を使うので、TeamListPage を StatefulWidget にします。
今の TeamListPage が仮の StatelessWidget の場合は、次の形に差し替えます。
class TeamListPage extends StatefulWidget {
const TeamListPage({super.key});
@override
State<TeamListPage> createState() => _TeamListPageState();
}
class _TeamListPageState extends State<TeamListPage> {
final teamNameController = TextEditingController();
bool isCreating = false;
String? errorText;
@override
void dispose() {
teamNameController.dispose();
super.dispose();
}
Future<void> logout() async {
await FirebaseAuth.instance.signOut();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bg,
appBar: AppBar(
title: const Text('トーク'),
actions: [
IconButton(
tooltip: 'ログアウト',
onPressed: logout,
icon: const Icon(Icons.logout),
),
],
),
body: const Center(
child: Text('ここにチーム一覧を表示します'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
StatefulWidget は、画面の中で変わる値を持てるWidgetです。
今回は、チーム名の入力、作成中かどうか、エラー文が変わるので使います。
Step 4:チーム作成画面を開く処理を作る
右下の + ボタンを押したら、チーム名を入力する画面を出します。
_TeamListPageState の中に、次の関数を追加します。
Future<void> showCreateTeamSheet() async {
teamNameController.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),
AppTextField(
controller: teamNameController,
label: 'チーム名',
),
if (errorText != null) ...[
const SizedBox(height: 12),
ErrorBox(message: errorText!),
],
const SizedBox(height: 16),
FilledButton(
onPressed: isCreating
? null
: () async {
await createTeam(setSheetState);
},
child: Text(isCreating ? '作成中...' : '作成する'),
),
],
),
);
},
);
},
);
}
showModalBottomSheet は、画面下から出てくる入力画面を表示する部品です。
StatefulBuilder は、ボトムシートの中だけを更新するために使います。
ボトムシートとは、画面下から出てくる小さな画面のことです。
Step 5:+ボタンとつなげる
FloatingActionButton を探してください。
今はこうなっているはずです。
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
これを次のように変更します。
floatingActionButton: FloatingActionButton(
onPressed: showCreateTeamSheet,
child: const Icon(Icons.add),
),
これで、右下の + ボタンを押すと、チーム作成画面が開きます。
Step 6:createTeam()を作る
次に、Firestoreにチームを作る処理を書きます。
_TeamListPageState の中に、次の関数を追加します。
Future<void> createTeam(StateSetter setSheetState) async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
setSheetState(() {
errorText = 'ログイン状態を確認できません。';
});
return;
}
final teamName = teamNameController.text.trim();
if (teamName.isEmpty) {
setSheetState(() {
errorText = 'チーム名を入力してください。';
});
return;
}
setSheetState(() {
isCreating = true;
errorText = null;
});
try {
final userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
final userData = userDoc.data();
final displayName = String.fromCharCodes(
(userData?['displayName'] ?? user.email ?? '名無し').toString().runes,
);
final email = String.fromCharCodes(
(userData?['email'] ?? user.email ?? '').toString().runes,
);
final teamRef = await FirebaseFirestore.instance.collection('teams').add({
'name': teamName,
'ownerId': user.uid,
'memberIds': [user.uid],
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
});
await teamRef.collection('members').doc(user.uid).set({
'uid': user.uid,
'email': email,
'displayName': displayName,
'role': 'owner',
'joinedAt': FieldValue.serverTimestamp(),
});
if (mounted) {
Navigator.of(context).pop();
}
} on FirebaseException catch (e) {
setSheetState(() {
errorText = e.message ?? 'チーム作成に失敗しました。';
});
} catch (_) {
setSheetState(() {
errorText = 'チーム作成に失敗しました。';
});
} finally {
if (mounted) {
setSheetState(() {
isCreating = false;
});
}
}
}
これで、チーム作成処理ができます。
Step 7:createTeam()の流れを見る
コード全体を短く見ると、こうです。
ログイン中ユーザーを取得する
↓
チーム名を取得する
↓
空欄チェックをする
↓
users/{uid} から名前とメールを取得する
↓
teams/{teamId} を作る
↓
teams/{teamId}/members/{uid} を作る
↓
ボトムシートを閉じる
この流れだけ覚えればOKです。
Step 8:ログイン中ユーザーを取得する
この部分です。
final user = FirebaseAuth.instance.currentUser;
currentUser は、現在ログインしているユーザーです。
ログインしていれば、uid や email が取れます。
ログインしていない場合は null になります。
if (user == null) {
setSheetState(() {
errorText = 'ログイン状態を確認できません。';
});
return;
}
return は、ここで処理を止めるという意味です。
Step 9:チーム名を確認する
この部分で、入力されたチーム名を取り出します。
final teamName = teamNameController.text.trim();
trim() は、前後の空白を消す処理です。
次に、空欄かどうかを確認します。
if (teamName.isEmpty) {
setSheetState(() {
errorText = 'チーム名を入力してください。';
});
return;
}
isEmpty は、文字が空かどうかを確認するものです。
Step 10:users/{uid} からプロフィールを取る
この部分です。
final userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
final userData = userDoc.data();
ここでは、前のページで保存した users/{uid} のプロフィールを取得しています。
そこから、名前とメールを取り出します。
final displayName = String.fromCharCodes(
(userData?['displayName'] ?? user.email ?? '名無し').toString().runes,
);
final email = String.fromCharCodes(
(userData?['email'] ?? user.email ?? '').toString().runes,
);
?? は、左側がなければ右側を使うという意味です。
たとえば、displayName がなければ、メールアドレスを使います。
それもなければ、名無し を使います。
Step 11:teams/{teamId} を作る
この部分が、チーム情報を作る処理です。
final teamRef = await FirebaseFirestore.instance.collection('teams').add({
'name': teamName,
'ownerId': user.uid,
'memberIds': [user.uid],
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
});
add() は、Firestoreに新しいドキュメントを作る命令です。
ここでは、teams コレクションの中に、新しいチームを作っています。
Firestoreが自動で teamId を作ります。
保存される形はこうです。
teams/{teamId}
name: 入力したチーム名
ownerId: 作成者のuid
memberIds: [作成者のuid]
createdAt: 作成日時
updatedAt: 更新日時
memberIds に自分の uid を入れることで、自分が参加しているチームとして検索できるようになります。
Step 12:members/{uid} を作る
次に、作成者をチームメンバーとして登録します。
await teamRef.collection('members').doc(user.uid).set({
'uid': user.uid,
'email': email,
'displayName': displayName,
'role': 'owner',
'joinedAt': FieldValue.serverTimestamp(),
});
teamRef は、さっき作ったチームの場所です。
その中に、members コレクションを作ります。
保存される形はこうです。
teams/{teamId}/members/{uid}
uid: 作成者のuid
email: 作成者のメール
displayName: 作成者の名前
role: owner
joinedAt: 参加日時
チームを作った人なので、role は owner にします。
Step 13:なぜ2つ同時に作るのか
チーム作成では、teams/{teamId} だけでは足りません。
理由は、チームの情報とメンバーの情報は役割が違うからです。
| 保存先 | 役割 |
|---|---|
teams/{teamId} | チーム名、作成者、参加者ID一覧を保存する |
teams/{teamId}/members/{uid} | チーム内での名前、メール、権限を保存する |
つまり、チームを作った瞬間に、
チームそのもの
チームに参加している自分
の両方を保存します。
Step 14:保存する
main.dart を保存します。
Macの場合:
command + S
Windowsの場合:
Ctrl + S
Step 15:実行する
ターミナルで実行します。
flutter run
すでに起動している場合は、ターミナルで r を押します。
r
動きがおかしい場合は、R でホットリスタートしてください。
R
Step 16:チーム作成を試す
アプリを起動します。
ログイン後、チーム一覧画面に進みます。
右下の + ボタンを押します。
+ ボタン
↓
新しいチームを作成
↓
チーム名を入力
↓
作成する
例として、次のように入力します。
開発チーム
作成できると、ボトムシートが閉じます。
Step 17:Firestoreで確認する
Firebase Consoleを開きます。
https://console.firebase.google.com/
次の場所を確認します。
Firestore Database
↓
データ
↓
teams
teams の中に新しいドキュメントができていれば成功です。
中身に、次があるか確認します。
name
ownerId
memberIds
createdAt
updatedAt
さらに、そのチームの中に members があるか確認します。
teams
└ teamId
└ members
└ uid
members/{uid} の中に、次があればOKです。
uid
email
displayName
role
joinedAt
よくあるエラーと直し方
| エラー | 原因 | 直し方 |
|---|---|---|
FirebaseFirestore isn't defined | importがない | cloud_firestore.dart をimportする |
FirebaseAuth isn't defined | importがない | firebase_auth.dart をimportする |
StateSetter isn't defined | Flutter Materialのimportがない | material.dart を確認する |
permission-denied | Firestore Rulesで拒否されている | 開発用Rulesを確認する |
| ボトムシートが開かない | onPressed が空のまま | onPressed: showCreateTeamSheet にする |
| チーム名エラーが出ない | setSheetState を使っていない | ボトムシート内では setSheetState を使う |
| 作成後に閉じない | Navigator.pop() がない | 成功後に Navigator.of(context).pop() を確認する |
permission-denied** が出たとき**
開発中だけ、Firestore Rulesを次のようにして確認できます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
これは開発用です。
本番公開では使わないでください。
本番では、ログインユーザーとチームメンバーだけが読み書きできるようにします。
setSheetState** を使う理由**
ボトムシートの中でエラー表示や「作成中…」を変えるには、setSheetState を使います。
通常の setState では、ボトムシート内の表示がすぐ変わらないことがあります。
setSheetState(() {
isCreating = true;
errorText = null;
});
ここは、今は深く理解しなくて大丈夫です。
ボトムシートの中を更新するときは setSheetState と覚えてください。
最短作業まとめ
読むのが大変な人は、ここだけ見てください。
1. importを確認
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
2. +ボタンでボトムシートを開く
floatingActionButton: FloatingActionButton(
onPressed: showCreateTeamSheet,
child: const Icon(Icons.add),
),
3. teamsを作る
final teamRef = await FirebaseFirestore.instance.collection('teams').add({
'name': teamName,
'ownerId': user.uid,
'memberIds': [user.uid],
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
});
4. membersを作る
await teamRef.collection('members').doc(user.uid).set({
'uid': user.uid,
'email': email,
'displayName': displayName,
'role': 'owner',
'joinedAt': FieldValue.serverTimestamp(),
});
5. 保存して実行
flutter run
チェックリスト
□ main.dartを開いた
□ cloud_firestoreをimportした
□ firebase_authをimportした
□ TeamListPageをStatefulWidgetにした
□ teamNameControllerを作った
□ showCreateTeamSheet()を作った
□ createTeam()を作った
□ +ボタンとshowCreateTeamSheetをつなげた
□ チーム名の空欄チェックを書いた
□ teams/{teamId} を作った
□ teams/{teamId}/members/{uid} を作った
□ roleをownerにした
□ 保存した
□ flutter runで起動した
□ Firestoreでteamsを確認した
□ Firestoreでmembersを確認した
ミニ確認問題
Q1. チーム作成時に作るFirestoreの場所はどこですか?
回答
次の2つです。
teams/{teamId}
teams/{teamId}/members/{uid}
Q2. チームを作った人の role は何にしますか?
回答
owner にします。
チームを作った人は、そのチームの所有者だからです。
Q3. memberIds は何のために使いますか?
回答
ログイン中のユーザーが参加しているチームを検索するために使います。
あとで、次のような検索に使います。
.where('memberIds', arrayContains: uid)
Q4. このページでnpmや環境変数は必要ですか?
回答
必要ありません。
このページでは、Firestoreにチーム情報とメンバー情報を作るコードを追加するだけです。
このページのまとめ
- チーム作成では、
teams/{teamId}とteams/{teamId}/members/{uid}を作る。 teams/{teamId}には、チーム名、作成者、参加者ID一覧を保存する。members/{uid}には、チーム内でのユーザー情報と権限を保存する。- チームを作った人は
ownerとして登録する。 memberIdsに自分のuidを入れる。memberIdsは、自分が参加しているチームを表示するために使う。- ボトムシート内の更新には
setSheetStateを使う。 - このページではnpmや環境変数は不要。
次のページでやること
次のページでは、作成したチームを一覧表示します。
where('memberIds', arrayContains: uid) を使って、自分が参加しているチームだけを表示します。
