【Flutter】FirebaseのAuthenticationでサインアップ後、Firestoreにユーザーデータを保存する

この記事でFirebaseのAuthを使って、ユーザー登録することはできたのですが、Authで登録ができてもDBに保存されるわけではないということに気付きました。

>[【Flutter】FirebaseのAuthでEmail&パスワード認証を実装する(後半)](https://qiita.com/iketeruhiyoko/items/7d0718bc6210ed545913)

ユーザー一覧を描画する時などにDBがないときついので、登録をした瞬間にDBに保存できるようにしていきます。

全体的に参考にしたのは下記の記事です。

>[Firebase User Authentication in Flutter](https://heartbeat.fritz.ai/firebase-user-authentication-in-flutter-1635fb175675)

## FirebaseとFlutterの統合

>[【Flutter】FirebaseのAuthでEmail&パスワード認証を実装する(後半)](https://qiita.com/iketeruhiyoko/items/7d0718bc6210ed545913)

Firebaseの設定はこちらの記事の最初のセクションを参考にしてください。

```dart:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_auth: ^0.15.4
```

```dart
import 'package:firebase_auth/firebase_auth.dart';
```
ライブラリを追加してファイルにインポートすれば完了です。

## 遷移ページの追加
```dart:main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LINE Clone',
      theme: ThemeData(
        primaryColor: Colors.blueGrey[800],
      ),
      home: RootPage(),
      routes: <String,WidgetBuilder>{
        '/home':(BuildContext context) => HomePage(),
        '/register':(BuildContext context) => RegisterPage(),
        '/login':(BuildContext context) => LoginPage(),
      }
    );
  }
}
```

Rootページも含め、ここで記入した4つのページを作成していきます。

先に何か適当なテキストだけを返すHome_page.dartファイルを作ります。

## Rootページでログインの有無を確認する
>One of the most common methods is to check the authentication status of the user in a Splash page. Then, depending on the status, we direct the user to the authentication flow (Login) or the main app flow (Home).

一般的な方法の一つにログインの有無をSplashページ(この記事ではRootページ)でチェックするという方法があります。その後、ログインの有無によってLoginページか、Homeページを返します。

```dart:root_page.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'home.dart';

class RootPage extends StatefulWidget {
  RootPage({Key key}) : super(key: key);

  @override
  _RootPageState createState() => _RootPageState();
}

class _RootPageState extends State<RootPage> {
  @override
  initState() {
    FirebaseAuth.instance
        .currentUser()
        .then((currentUser) => {
              if (currentUser == null)
                {Navigator.pushReplacementNamed(context, "/login")}
              else
                {
                  Firestore.instance
                      .collection("users")
                      .document(currentUser.uid)
                      .get()
                      .then((DocumentSnapshot result) =>
                          Navigator.pushReplacement(
                              context,
                              MaterialPageRoute(
                                  builder: (context) => HomePage(
                                        uid: currentUser.uid,
                                      ))))
                      .catchError((err) => print(err))
                }
            })
        .catchError((err) => print(err));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: Text("Loading..."),
        ),
      ),
    );
  }
}
```

まず、ログインの有無の確認、つまり、`Firebase.instance.currentUser`でログインユーザーを取って来させる。

その後、`currnetUser`を取って来れなかったらLoginページへ遷移する。

```dart
  FirebaseAuth.instance
        .currentUser()
        .then((currentUser) => {
              if (currentUser == null)
                {Navigator.pushReplacementNamed(context, "/login")}
```

というコードになっています。

Flutterでは主に`Navigator`を利用して画面遷移を実装するのですが、`pushReplacementNamed`はRootページのように一方通行、つまり、戻ることを想定していない時に利用します。

>参考: [FlutterのNavigatorで画面遷移](https://medium.com/@katsummy/flutter%E3%81%AEnavigator%E3%81%A7%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB-c3f5a358a54d)

```dart
 else
  {
    Firestore.instance
      .collection("users")
      .document(currentUser.uid)
      .get()
      .then((DocumentSnapshot result) =>
          Navigator.pushReplacement(
              context,
               MaterialPageRoute(
                 builder: (context) => HomePage(
                 uid: currentUser.uid,
                      ))))
                  .catchError((err) => print(err))
                }
            })
        .catchError((err) => print(err));
    super.initState();
  }
```

`currentUser`を取って来れたらFirestoreから`users collection`の`currentUser.uid`を取ってくる。

これはそのuserの全てのデータが入っているオブジェクトです。その後、Homeページへ遷移します。

Loginページの遷移と違い、`MaterialPageRoute`での遷移になりますがこれはios風な遷移を実現するものとなっています。

>サンプルコードでは、pushメソッドの引数としてMaterialPageRouteを指定しています。これは、遷移時にMaterialDesignに則ったアニメーションを行うための指定です。
MaterialPageRouteをCupertinoPageRouteと書き換えれば、iOS風のアニメーションで遷移します。
参考: [FlutterのNavigatorの使い方と仕組み](https://qiita.com/heavenosk/items/9e43298955a371221393)

UIは遷移までの少しの時間`Loading...`というテキストを返すだけのものとしています。

## Registerページの作成
```dart:register_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'home_page.dart';

class RegisterPage extends StatefulWidget {
  RegisterPage({Key key}) : super(key: key);

  @override
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {

  final GlobalKey<FormState> _registerFormKey = GlobalKey<FormState>();

  TextEditingController nameInputController;
  TextEditingController emailInputController;
  TextEditingController pwdInputController;

  @override
  initState() {
    nameInputController = new TextEditingController();
    emailInputController = new TextEditingController();
    pwdInputController = new TextEditingController();
    super.initState();
  }

  String emailValidator(String value) {
    Pattern pattern =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
    RegExp regex = new RegExp(pattern);
    if (!regex.hasMatch(value)) {
      return '正しいEmailのフォーマットで入力してください';
    } else {
      return null;
    }
  }

  String pwdValidator(String value) {
    if (value.length < 8) {
      return 'パスワードは8文字以上で入力してください';
    } else {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("アカウント作成"),
        ),
        body: registerscreen(),
      );
  }

  Widget registerscreen() {
   return Container(
            padding: const EdgeInsets.all(20.0),
            child: SingleChildScrollView(
                child: Form(
              key: _registerFormKey,
              child: Column(
                children: <Widget>[
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Name*', hintText: "Lebron"),
                    controller: nameInputController,
                    validator: (value) {
                      if (value.length < 3) {
                        return "名前は3文字以上で入力してください";
                      }
                    },
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Email*', hintText: "lebron.james@gmail.com"),
                    controller: emailInputController,
                    keyboardType: TextInputType.emailAddress,
                    validator: emailValidator,
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Password*', hintText: "********"),
                    controller: pwdInputController,
                    obscureText: true,
                    validator: pwdValidator,
                  ),
                  Padding(
                    padding: EdgeInsets.all(10.0)
                  ),
                  RaisedButton(
                    child: Text(
                      "アカウント作成",
                      style: TextStyle(
                        fontSize: 20.0,
                      ),
                    ),
                    color: Theme.of(context).primaryColor,
                    textColor: Colors.white,
                    onPressed: () {
                      if (_registerFormKey.currentState.validate()) {
                          FirebaseAuth.instance
                            .createUserWithEmailAndPassword(
                              email: emailInputController.text,
                              password: pwdInputController.text)
                            .then((currentUser) => Firestore.instance
                              .collection("users")
                              .document(currentUser.user.uid)
                              .setData({
                                "uid": currentUser.user.uid,
                                "name": nameInputController.text,
                                "email": emailInputController.text,
                              })
                            .then((result) => {
                              Navigator.pushAndRemoveUntil(
                                  context,
                                  MaterialPageRoute(
                                      builder: (context) => HomePage(
                                            uid: currentUser.user.uid,
                                          )),
                                  (_) => false),
                                    nameInputController.clear(),
                                    emailInputController.clear(),
                                    pwdInputController.clear(),
                                  })
                              .catchError((err) => print(err)))
                          .catchError((err) => print(err));
                      }
                    },
                  ),
                  FlatButton(
                    child: Text(
                      "ログイン",
                      style: TextStyle(
                        fontSize: 20.0,
                        color: Colors.blue  
                      ),
                    ),
                    onPressed: () {
                      Navigator.pop(context);
                    },
                  )
                ],
              ),
            )
          )
        );
  }
}
```
UIはこんな感じになります。
<img width="336" alt="スクリーンショット 2020-03-19 13.13.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487891/bc63d455-7ca3-310c-98f5-0a4c6013d086.png">



```dart
RegisterPage({Key key}) : super(key: key);
```

これが何を意味しているか分からなかったので調べていたのですが、

>・MyHomePage({Key key, this.title})
ここは名前付き引数でコンストラクタを設定しています
またAutomatic field initializationを利用しているのでtitleに入ってくる値は自動でフィールドに代入してくれます。
・: super(key: key);
これはMyHomePageのコンストラクタを呼び出す際のリダイレクト先を設定しています。
つまり、Redirecting Constructorsの利用です。
リダイレクト先はsuper(Key:key)となっているため、継承元のコンストラクタを設定（super()という呼び出しが継承元のコンストラクタを呼び出していること）しているということになります。

>[【初心者向け】FlutterサンプルコードのMyHomePage({Key key, this.title}) : super(key: key);って何？](https://note.com/zutai_dekame/n/nabf22f1a3eec)

うーん、分かるようで分からない...

記事で詳しく説明してくれているので参考にしてみてください。

```dart
class _RegisterPageState extends State<RegisterPage> {

  final GlobalKey<FormState> _registerFormKey = GlobalKey<FormState>();

  TextEditingController nameInputController;
  TextEditingController emailInputController;
  TextEditingController pwdInputController;

  @override
  initState() {
    nameInputController = new TextEditingController();
    emailInputController = new TextEditingController();
    pwdInputController = new TextEditingController();
    super.initState();
  }
```

`Key`はWidgetが動き回る時にステートを保持するもので、`GlobalKey`はアプリ内の階層を問わずステートを保ったまま、ウィジェットに親を変更させることが出来ます。

>参考: [【書き起こし】key の使い時 - Flutter ウィジェット 101（初級編）Ep.4](https://qiita.com/saneatsu/items/77572524eead92787ce8#globalkey)

そして`TextEditingController`によってフォームの入力を管理し、入力したものをそのままDBに保存したりできるようになります。

```dart
String emailValidator(String value) {
    Pattern pattern =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
    RegExp regex = new RegExp(pattern);
    if (!regex.hasMatch(value)) {
      return '正しいEmailのフォーマットで入力してください';
    } else {
      return null;
    }
  }

  String pwdValidator(String value) {
    if (value.length < 8) {
      return 'パスワードは8文字以上で入力してください';
    } else {
      return null;
    }
  }
```

ここでemailとpasswordそれぞれバリデーションの実装をします。

`RegExp regex = new RegExp(pattern);`は正規表現を作成しています。


>*正規表現とは？
様々な文字列を一つの文字列で表現する表記法。
参考: https://qiita.com/soarflat/items/2b50c1efe2ea54a762d7

その後、各フォームで先ほど定義したcontrollerやvalidationを指定していきます。

```dart
onPressed: () {
      if (_registerFormKey.currentState.validate()) {
         FirebaseAuth.instance
            .createUserWithEmailAndPassword(
              email: emailInputController.text,
              password: pwdInputController.text)
            .then((currentUser) => Firestore.instance
              .collection("users")
              .document(currentUser.user.uid)
              .setData({
                 "uid": currentUser.user.uid,
                  "name": nameInputController.text,
                  "email": emailInputController.text,
                   })
            .then((result) => {
               Navigator.pushAndRemoveUntil(
                  context,
                  MaterialPageRoute(
                     builder: (context) => HomePage(
                     uid: currentUser.user.uid,
                        )),
                 (_) => false),
                     nameInputController.clear(),
                     emailInputController.clear(),
                     pwdInputController.clear(),
                    })
                 .catchError((err) => print(err)))
             .catchError((err) => print(err));
          }
        },
      ),
```

ボタンを押した時の処理です。

アカウント作成ボタンを押した時、フォームは入力された値がvalidationを通っているかを確認します。

通っていなかった場合には警告がユーザーに表示されます。

>If they do match, the method `createUserWithEmailAndPassword` of the `FirebaseAuth` instance is used to signup a new user.

validationを通過した場合、`FirebaseAuth` instanceの`createUserWithEmailAndPassword`メソッドが新しいユーザーをサインアップするために使われます。
(参考記事と全く同じ実装をしているわけではないので訳を上述のコードと対応させています)

>If the user is successfully signed up, a new document is created containing their first and last names, email, and uid (the same mentioned above in the Splash page). To make it easier to get this new object from Cloud Firestore, the document ID is the user’s uid.

ユーザーのサインアップが成功した場合、名前とemail、パスワード、そしてuidを含んだ新しいドキュメントが作成されます。

この新しいオブジェクトをCloud Firestoreから取得することは簡単にするためにドキュメントのIDはユーザーのuidとなっています。

そして全てのアクションが成功するとユーザーはホームページに遷移します。

## Loginページの作成
```dart:login_page.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'home_page.dart';

class LoginPage extends StatefulWidget {
  LoginPage({Key key}) : super(key: key);

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>();
  TextEditingController emailInputController;
  TextEditingController pwdInputController;

  @override
  initState() {
    emailInputController = new TextEditingController();
    pwdInputController = new TextEditingController();
    super.initState();
  }

  String emailValidator(String value) {
    Pattern pattern =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
    RegExp regex = new RegExp(pattern);
    if (!regex.hasMatch(value)) {
      return '正しいEmailのフォーマットで入力してください';
    } else {
      return null;
    }
  }

  String pwdValidator(String value) {
    if (value.length < 8) {
      return 'パスワードは8文字以上で入力してください';
    } else {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("ログイン"),
        ),
        body: loginscreen()
    );
  }

  Widget loginscreen() {
    return Container(
            padding: const EdgeInsets.all(20.0),
            child: SingleChildScrollView(
                child: Form(
              key: _loginFormKey,
              child: Column(
                children: <Widget>[
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Email*', hintText: "lebron.james@gmail.com"),
                    controller: emailInputController,
                    keyboardType: TextInputType.emailAddress,
                    validator: emailValidator,
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Password*', hintText: "********"),
                    controller: pwdInputController,
                    obscureText: true,
                    validator: pwdValidator,
                  ),
                  Padding(
                    padding: EdgeInsets.all(10.0)
                  ),
                  RaisedButton(
                    child: Text(
                      "ログイン",
                      style: TextStyle(
                        fontSize: 20.0
                      ),
                    ),
                    color: Theme.of(context).primaryColor,
                    textColor: Colors.white,
                    onPressed: () {
                      if (_loginFormKey.currentState.validate()) {
                        FirebaseAuth.instance
                            .signInWithEmailAndPassword(
                              email: emailInputController.text,
                              password: pwdInputController.text)
                            .then((currentUser) => Firestore.instance
                              .collection("users")
                              .document(currentUser.user.uid)
                              .get()
                              .then((DocumentSnapshot result) =>
                                Navigator.pushReplacement(
                                    context,
                                    MaterialPageRoute(
                                        builder: (context) => HomePage(
                                              uid: currentUser.user.uid,
                                            ))))
                                .catchError((err) => print(err)))
                            .catchError((err) => print(err));
                      }
                    },
                  ),
                  FlatButton(
                    child: Text(
                      "アカウントを作成する",
                      style: TextStyle(
                        fontSize: 20,
                        color: Colors.blue
                      ),
                    ),
                    onPressed: () {
                      Navigator.pushNamed(context, "/register");
                    },
                  )
                ],
              ),
            )));
  }
}
```

>Both `TextFormFields` have their own controllers and validation methods, like the Register page. However, the `RaisedButton` Widget is used to log in the user. The `FlatButton` Widget included redirects the user to the Register page if they don’t have an account. The form has its own `GlobalKey`, to keep track of the forms state.

Registerページと同じようにemailとpasswordのTextFormFieldsはそれぞれのcontrollerとvalidationメソッドを持っています。

しかし、`RaisedButton`はloginユーザーを使用し、`FlatButton`はユーザーがアカウントを持っていなかった場合、Registerページへ遷移させます。

これは各フォームがそれぞれの`GlobalKey`を持ち、フォームの状態を持っているためです。

後はほとんどRegisterページと同じ実装になります。

## Firebaseを確認する
<img width="334" alt="スクリーンショット 2020-03-19 13.16.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487891/6b759cca-52eb-e668-8234-c2c13d6331dd.png">

各フォームの項目をそれぞれ入力して、Homeページに遷移されることを確認します。

その後、Firebaseに移動して、下記のようにDBが作成され、入力した値が反映されていれば成功です。

<img width="419" alt="スクリーンショット 2020-03-19 13.16.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487891/c0bf0334-fb35-7ece-9d46-b1e68edcc60b.png">