Flutter×FirebaseのAuthとCRUD処理まとめ

<img width="300" alt="Firebase_auth_crud" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407895/bc4192a2-0c71-4121-d04d-9bae53433e1e.jpeg">

M1 MacBook、Android Studio、Flutter 2.0
2021年1月末からプログラミング学習始めた初心者です。自分用メモ。

https://qiita.com/igakeso/items/2c567176326f783801d6
前回、Flutter×FirebaseのCRUD処理をまとめたのですが、Authのuidと紐付いていないことに気付いたため、FrebaseAuthのuidと共通でFirestoreにusersのcollectionを作り、そのサブコレクションにtodoのcollectionを作り、サブコレクションに対してCRUDを行う処理をまとめました。

1、FlutterとFIrebaseのセットアップ
https://firebase.google.com/docs/flutter/setup
2、セットアップ後、main.dartだけで動くコードです。


```
  firebase_core: ^1.3.0
  firebase_auth: ^1.4.1
  cloud_firestore: ^2.2.2
```

```
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Auth CRUD',
      theme: ThemeData(
        backgroundColor: Colors.blue,
      ),
      home: AuthPage(),
    );
  }
}

class AuthPage extends StatefulWidget {
  @override
  _AuthPageState createState() => _AuthPageState();
}

class _AuthPageState extends State<AuthPage> {
  late TextEditingController nameInputController = TextEditingController();
  late TextEditingController emailInputController = TextEditingController();
  late TextEditingController passwordInputController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ログイン画面'),
      ),
      body: Center(
        child: Container(
          padding: const EdgeInsets.all(20),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Image.network(
                  'https://1.bp.blogspot.com/-UG7cj_zxYes/X4aVsGG-tgI/AAAAAAABbys/BpJ8aaXO6vALEcBTUJtPzo-0sBQaOT69wCNcBGAsYHQ/s862/ofuro_sauna_door.png',
                  height: 200.0),
              TextFormField(
                controller: nameInputController,
                decoration: const InputDecoration(labelText: 'お名前（初回登録時のみ入力）'),
              ),
              TextFormField(
                controller: emailInputController,
                decoration: const InputDecoration(labelText: 'メールアドレス'),
                keyboardType: TextInputType.emailAddress,
              ),
              TextFormField(
                controller: passwordInputController,
                decoration: const InputDecoration(labelText: 'パスワード（6文字以上）'),
                obscureText: true,
              ),
              const SizedBox(height: 20),
              Container(
                width: double.infinity,
                child: ElevatedButton(
                  child: const Text('ユーザー登録'),
                  onPressed: () async {
                    try {
                      await FirebaseAuth.instance.createUserWithEmailAndPassword(
                              email: emailInputController.text,
                              password: passwordInputController.text);
                      await FirebaseFirestore.instance.collection("users").doc(FirebaseAuth.instance.currentUser!.uid).set({
                        'name': nameInputController.text,
                        'email': emailInputController.text,
                      });
                    } catch (e) {
                      print(e);
                    }
                  },
                ),
              ),
              const SizedBox(height: 10),
              Container(
                width: double.infinity,
                child: ElevatedButton(
                  child: const Text('ログイン'),
                  onPressed: () async {
                    try {
                      await FirebaseAuth.instance.signInWithEmailAndPassword(
                          email: emailInputController.text,
                          password: passwordInputController.text);
                      final loginUser = FirebaseAuth.instance.currentUser;
                      await FirebaseFirestore.instance.collection("users").doc(loginUser!.uid).get();
                      Navigator.of(context).pushReplacement(MaterialPageRoute(
                          builder: (context) => HomePage(loginUser)));
                    } catch (e) {
                      print(e);
                    }
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage(this.loginUser);
  late User loginUser;

  @override
  _HomePageState createState() => _HomePageState(loginUser);
}

class _HomePageState extends State<HomePage> {
  _HomePageState(this.loginUser);
  User loginUser;
  TextEditingController createTodoController = TextEditingController();
  TextEditingController updateTodoController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ログイン後画面'),
      ),
      body: SafeArea(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: Text('Login User uid: ' + loginUser.uid),
            ),
            Image.network('https://1.bp.blogspot.com/-f3RpEHd25R4/X79cj7SEo_I/AAAAAAABce8/tPhWRoxKbOAvUNRcwqxK1YmVyKrKCNapwCNcBGAsYHQ/s762/ofuro_sauna_gaikiyoku.png',
                height: 200.0),
            Container(
              child: Expanded(
                child: StreamBuilder<QuerySnapshot>(
                    stream: FirebaseFirestore.instance.collection('users').doc(loginUser.uid).collection('todo').snapshots(),
                    builder: (context, snapshot) {
                      if (snapshot.hasData) {
                        return ListView.builder(
                          itemBuilder: (BuildContext context, int index) {
                            return CheckboxListTile(
                              controlAffinity: ListTileControlAffinity.leading,
                              title: Text(snapshot.data!.docs[index]['title']),
                              value: snapshot.data!.docs[index]['isDone'],
                              onChanged: (value) {
                                snapshot.data!.docs[index].reference.update({
                                  'isDone': value,
                                  'updatedTime': Timestamp.now(),
                                });
                              },
                              secondary: IconButton(
                                icon: const Icon(Icons.more_horiz),
                                onPressed: () {
                                  showModalBottomSheet(
                                      context: context,
                                      builder: (context) {
                                        return SafeArea(
                                          child: Column(
                                            mainAxisSize: MainAxisSize.min,
                                            children: [
                                              ListTile(
                                                title: const Text('編集'),
                                                leading: const Icon(Icons.edit),
                                                onTap: () {
                                                  Navigator.pop(context);
                                                  showDialog(
                                                      context: context,
                                                      builder: (context) {
                                                        return SimpleDialog(
                                                          title: Container(
                                                            child: Column(
                                                              children: [
                                                                const Text('編集'),
                                                                Container(
                                                                  child: TextField(
                                                                    controller: updateTodoController,
                                                                    decoration: const InputDecoration(
                                                                        border: OutlineInputBorder()
                                                                    ),
                                                                  ),
                                                                ),
                                                                ElevatedButton(
                                                                  onPressed:
                                                                      () async {
                                                                    await snapshot.data!.docs[index].reference.update({
                                                                      'title': updateTodoController.text,
                                                                      'updatedTime': Timestamp.now(),
                                                                    });
                                                                    Navigator.pop(context);
                                                                  },
                                                                  child: const Text('編集'),
                                                                )
                                                              ],
                                                            ),
                                                          ),
                                                        );
                                                      });
                                                },
                                              ),
                                              ListTile(
                                                title: const Text('削除'),
                                                leading: const Icon(Icons.delete),
                                                onTap: () {
                                                  Navigator.pop(context);
                                                  showDialog(
                                                      context: context,
                                                      builder: (context) {
                                                        return AlertDialog(
                                                          title: Text(
                                                              '${snapshot.data!.docs[index]['title']}を削除実行しますか？'
                                                          ),
                                                          actions: [
                                                            TextButton(
                                                              onPressed:
                                                                  () async {
                                                                await snapshot.data!.docs[index].reference.delete();
                                                                Navigator.pop(context);
                                                              },
                                                              child: const Text('削除実行'),
                                                            ),
                                                            TextButton(
                                                              onPressed: () {
                                                                Navigator.pop(context);
                                                              },
                                                              child: const Text('キャンセル'),
                                                            ),
                                                          ],
                                                        );
                                                      });
                                                },
                                              )
                                            ],
                                          ),
                                        );
                                      });
                                },
                              ),
                            );
                          },
                          itemCount: snapshot.data!.docs.length,
                        );
                      } else {
                        return Container();
                      }
                    }),
              ),
            ),
            Container(
              padding: const EdgeInsets.only(left: 20, right:20),
              width: double.infinity,
              child: ElevatedButton(
                  child: const Text('ログアウト'),
                  onPressed: () async {
                    await FirebaseAuth.instance.signOut();
                    Navigator.of(context).pop();
                  }),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () {
            showModalBottomSheet(
                context: context,
                builder: (context) {
                  return SafeArea(
                    child: Column(mainAxisSize: MainAxisSize.min, children: [
                      const Padding(
                        padding: EdgeInsets.all(10.0),
                        child: Text('ToDo新規登録'),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(10.0),
                        child: Text('Login User uid: ' + loginUser.uid),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(20.0),
                        child: Container(
                          child: TextField(
                            controller: createTodoController,
                            decoration: const InputDecoration(
                                border: OutlineInputBorder()
                            ),
                          ),
                        ),
                      ),
                      Container(
                        padding: const EdgeInsets.only(left:20, right:20),
                        width: double.infinity,
                        child: ElevatedButton(
                          child: const Text('新規登録'),
                          onPressed: () async {
                            await FirebaseFirestore.instance.collection("users").doc(loginUser.uid).collection('todo').add({
                              'title': createTodoController.text,
                              'isDone': false,
                              'createdTime': Timestamp.now()
                            });
                            Navigator.pop(context);
                          },
                        ),
                      ),
                      Container(
                        width: double.infinity,
                        padding: const EdgeInsets.only(left:20, right:20),
                        child: ElevatedButton(
                          child: const Text('戻る'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ),
                    ]
                    ),
                  );
                });
          }),
    );
  }
}
```