【LINE風追加UI】ボトムシートでメンバー追加画面を作る
このページでやること
このページでは、メンバー追加画面をLINE風のボトムシートUIに整えます。
前のページでは、メールアドレスから登録済みユーザーを検索して、チームに追加する処理を作りました。
今回は、その入力画面を見やすくします。
メンバー一覧画面
↓
右下の+を押す
↓
下からLINE風の追加画面が出る
↓
メールアドレスを入力
↓
追加する
今日のゴール
右下の + ボタンを押すと、下からこのような画面が出るようにします。
メンバーを追加
登録済みユーザーのメールアドレスを入力してください。
メールアドレス
[ example@example.com ]
[追加する]
LINE風に、白背景・上だけ角丸・シンプルな入力欄で作ります。
npmや環境変数は必要?
このページでは、npmは使いません。
環境変数も設定しません。
必要なのは、すでにあるFlutterのコードだけです。
main.dartを開く
↓
MemberListPageを探す
↓
showAddMemberSheet()を作る
↓
右下の+ボタンにつなげる
↓
保存して確認
Step 1:main.dartを開く
次のファイルを開きます。
lib/main.dart
ターミナルから開く場合はこちらです。
code lib/main.dart
code が使えない場合は、VS Codeの左側から lib/main.dart を開いてください。
Step 2:MemberListPageを探す
main.dart の中で、次を検索します。
class MemberListPage
VS Codeなら、次で検索できます。
command + F
この MemberListPage が、メンバー一覧画面です。
ここに、メンバー追加用のボトムシートを作ります。
Step 3:入力用Controllerを確認する
_MemberListPageState の中に、次があるか確認します。
final emailController = TextEditingController();
これは、メールアドレス入力欄の文字を管理するための道具です。
まだない場合は、次のように追加します。
class _MemberListPageState extends State<MemberListPage> {
final emailController = TextEditingController();
bool isAdding = false;
String? errorText;
@override
void dispose() {
emailController.dispose();
super.dispose();
}
// この下に処理を書いていきます
}
dispose() は、使い終わった入力欄を片付ける処理です。
Step 4:ボトムシートを開く関数を作る
_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: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 44,
height: 5,
margin: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(999),
),
alignment: Alignment.center,
),
const Text(
'メンバーを追加',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: AppColors.text,
),
),
const SizedBox(height: 8),
const Text(
'登録済みユーザーのメールアドレスを入力してください。',
style: TextStyle(
color: AppColors.subText,
height: 1.5,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
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 ? '追加中...' : '追加する'),
),
],
),
),
);
},
);
},
);
}
これで、下からLINE風のメンバー追加画面が出ます。
Step 5:LINE風に見せるポイント
LINE風に見せるポイントは、次の4つです。
| 場所 | 内容 |
|---|---|
| 背景 | 白 |
| 形 | 上だけ角丸 |
| 余白 | 広めに取る |
| 入力欄 | シンプルにする |
中心は、この部分です。
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
上だけ角丸にすることで、下から出てくる画面らしくなります。
Step 6:上の小さなバーを作る
この部分です。
Container(
width: 44,
height: 5,
margin: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(999),
),
alignment: Alignment.center,
),
これは、ボトムシートの上にある小さなバーです。
────
このバーがあると、「下から出てきた画面」という雰囲気が出ます。
Step 7:キーボードに隠れないようにする
この部分です。
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
スマホでキーボードが出たとき、入力欄が隠れないようにする設定です。
メール入力欄をタップ
↓
キーボードが出る
↓
ボトムシートが上にずれる
初心者の方は、このまま使えばOKです。
Step 8:SingleChildScrollViewを入れる理由
この部分です。
SingleChildScrollView(
child: Column(
...
),
)
スマホ画面が小さいと、ボトムシートの中身が入りきらないことがあります。
SingleChildScrollView を使うと、縦にスクロールできるようになります。
画面が小さい
↓
スクロールできる
↓
入力欄やボタンが見える
Step 9:入力欄を作る
メールアドレス入力欄は、この部分です。
AppTextField(
controller: emailController,
label: 'メールアドレス',
keyboardType: TextInputType.emailAddress,
),
keyboardType: TextInputType.emailAddress にすると、スマホでメール入力向けのキーボードになります。
@ が入力しやすい
メールアドレス入力に向いている
Step 10:エラー表示を作る
この部分です。
if (errorText != null) ...[
const SizedBox(height: 12),
ErrorBox(message: errorText!),
],
エラーがあるときだけ、赤いエラー表示を出します。
たとえば、メールアドレスが空のときです。
メールアドレスを入力してください。
登録済みユーザーが見つからないときも、ここに表示します。
このメールアドレスのユーザーは見つかりません。
Step 11:追加ボタンを作る
この部分です。
FilledButton(
onPressed: isAdding
? null
: () async {
await addMemberByEmail(setSheetState);
},
child: Text(isAdding ? '追加中...' : '追加する'),
),
意味はこうです。
追加中ではない
↓
ボタンを押せる
↓
addMemberByEmailが動く
追加中
↓
ボタンを押せない
↓
「追加中...」と表示
二重クリックで何度も追加されるのを防ぎます。
Step 12:追加処理があるか確認する
前のページで作った addMemberByEmail() があるか確認します。
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 13:右下の+ボタンにつなげる
MemberListPage の Scaffold に、次があるか確認します。
floatingActionButton: FloatingActionButton(
onPressed: showAddMemberSheet,
child: const Icon(Icons.add),
),
これで、右下の + を押すと、メンバー追加ボトムシートが開きます。
Step 14:保存する
main.dart を保存します。
Macの場合:
command + S
Windowsの場合:
Ctrl + S
Step 15:実行する
ターミナルで実行します。
flutter run
すでに起動している場合は、ターミナルで r を押します。
r
うまく反映されない場合は、R を押します。
R
Step 16:画面を確認する
アプリを開きます。
ログイン
↓
トーク一覧
↓
チームをタップ
↓
右上のメンバーアイコン
↓
右下の+
下から、メンバー追加画面が出れば成功です。
メンバーを追加
登録済みユーザーのメールアドレスを入力してください。
メールアドレス
追加する
Step 17:空欄エラーを確認する
何も入力せずに「追加する」を押します。
次のエラーが表示されればOKです。
メールアドレスを入力してください。
Step 18:未登録ユーザーのエラーを確認する
存在しないメールアドレスを入力します。
notfound@example.com
「追加する」を押します。
次のエラーが表示されればOKです。
このメールアドレスのユーザーは見つかりません。
Step 19:登録済みユーザーを追加する
登録済みのメールアドレスを入力します。
sato@example.com
「追加する」を押します。
ボトムシートが閉じて、メンバー一覧に表示されれば成功です。
佐藤 花子
sato@example.com
メンバー
よくあるエラーと直し方
| エラー | 原因 | 直し方 |
|---|---|---|
showAddMemberSheet isn't defined | 関数がない | showAddMemberSheet() を追加 |
emailController isn't defined | Controllerがない | emailController を追加 |
addMemberByEmail isn't defined | 追加処理がない | addMemberByEmail() を追加 |
| ボトムシートが開かない | FABにつながっていない | onPressed: showAddMemberSheet にする |
| キーボードで隠れる | 余白設定がない | viewInsets.bottom + 20 を入れる |
| 追加ボタンが反応しない | isAdding がtrueのまま | finally でfalseに戻す |
| ユーザーが見つからない | usersに登録されていない | 先に新規登録する |
permission-denied が出たとき
開発中だけ、Firestore Rulesを次のようにして確認できます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
これは学習用です。
本番公開では使わないでください。
本番では、owner または admin だけがメンバー追加できるようにします。
最短作業まとめ
読むのが大変な人は、ここだけ見てください。
1. ボトムシートを作る
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: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'メンバーを追加',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 12),
AppTextField(
controller: emailController,
label: 'メールアドレス',
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
FilledButton(
onPressed: isAdding
? null
: () async {
await addMemberByEmail(setSheetState);
},
child: Text(isAdding ? '追加中...' : '追加する'),
),
],
),
),
);
},
);
},
);
}
2. 右下の+につなげる
floatingActionButton: FloatingActionButton(
onPressed: showAddMemberSheet,
child: const Icon(Icons.add),
),
3. 保存して実行
flutter run
チェックリスト
□ main.dartを開いた
□ MemberListPageを探した
□ emailControllerを確認した
□ showAddMemberSheet()を作った
□ showModalBottomSheetを書いた
□ 上だけ角丸にした
□ キーボード対策を入れた
□ SingleChildScrollViewを入れた
□ メールアドレス入力欄を作った
□ エラー表示を入れた
□ 追加ボタンを作った
□ addMemberByEmail()につなげた
□ FloatingActionButtonにつなげた
□ 保存した
□ flutter runで確認した
□ 右下の+からボトムシートが出た
ミニ確認問題
Q1. ボトムシートを表示する命令は何ですか?
回答
showModalBottomSheet() です。
Q2. メールアドレス入力欄の文字を管理するものは何ですか?
回答
TextEditingController です。
今回なら emailController を使います。
Q3. キーボードで入力欄が隠れにくくする設定は何ですか?
回答
次の設定です。
bottom: MediaQuery.of(context).viewInsets.bottom + 20
Q4. このページでnpmや環境変数は必要ですか?
回答
必要ありません。
FlutterのUIを整えるだけです。
このページのまとめ
- メンバー追加画面はボトムシートで作る。
showModalBottomSheet()で下から画面を出す。backgroundColor: Colors.whiteで白背景にする。shapeで上だけ角丸にする。MediaQuery.of(context).viewInsets.bottomでキーボード対策をする。emailControllerで入力されたメールアドレスを管理する。FilledButtonからaddMemberByEmail()を呼び出す。- このページではnpmや環境変数は不要。
次のページでやること
次のページでは、メンバーの権限を変更できるようにします。
member を admin にしたり、viewer に変更したりできる画面を作ります。
