FlutterでFirebase Authenticationを使ったログインUIを作成してみた。

## はじめに
Flutterを使ってネイティブアプリを作っているが、Widgetの使い方やFirestoreとのデータ連携の仕方など、コードの書き方が分からずそれなりに苦労している。
一旦分かってしまえばかなり高速でアプリ開発ができそうなので、使い方やコードの書き方のメモを残しておく。
今回は`Firesbase Authentication` を使ったログイン機能を実装してみたので、その時の手順とコードメモ。

## Flutterの実行環境

- Ubuntu 18.04LTS（GCP上）
- Flutter 1.22.6
- Dart 2.10.5
- Android Studio 4.1.2
- VScode 1.53.0
　　

## メモ内容
今回は少しステップが多かったので、簡単に章分け。
１．Firebase AuthenticationをFlutterから呼び出すための設定
２．各種dartコードの作成


## １．Firebase AuthenticationをFlutterから呼び出すための設定
基本的にFirebaseを使うまでの設定は、[**【FlutterからCloud Firestoreのデータ取得 ＆ データ書き込み】**](https://qiita.com/smiler5617/items/8dcf720f5477a5f4b7d7)の１章を参考に。

ただし、`プロジェクトディレクトリ > pubspec.yaml` は以下の様に編集する。

```yaml:pubspec.yaml
# --- 省略 ---

dependencies:
  flutter:

  # firebase
  firebase_core: ^0.4.0+9
  firebase_auth: ^0.14.0+5
  cloud_firestore: ^0.13.5

# --- 省略 ---
```

必要なパッケージをインストール

```bash:TERMINALより実行（本記事ではVScode上で実行）
$ flutter pub get
```


一旦ここで`main.dart`にimport文を追加して保存したら、`flutter run` を実行し、問題なくパッケージが読み込めているか確認。

```dart:main.dart
import 'package:flutter/material.dart';

// ここが追記部分！　Cloud Firestoreに繋ぐためのパッケージをインポート。
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

//--- 省略 ---
```

## ２．各種dartコードの作成
今回作成してみたログイン機能の画面レイアウトや画面遷移は以下の様な感じ。
　※Twitter や Facebook を参考に作成してみた。
![Qiita-No036_img01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/632517c8-763b-c7c4-15b6-c4fcf1ebac7b.jpeg)



上記の構成を作るために以下のdartファイルを作成。
・`main.dart` ⇒ `flutter run` コマンドで最初に呼び出されるファイル。
・`login.dart` ⇒ 初期画面（上の一番左の図）。
・`registration.dart` ⇒ アカウント登録画面（真ん中の図）。
・`authentication_error.dart` ⇒ エラーメッセージを日本語で表示するためのクラス。
・`home.dart` ⇒ ログイン後 or 登録後の画面（一番右の図）。
　

初めてなので細かい作り込みはせずに、単純なアカウント作成機能とログイン機能を実装。
以下、各`dart`ファイルのコードを貼り付ける。

```dart:main.dart
import 'package:flutter/material.dart';
import 'login_parts/login.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Auth',
      home: Login(),
    );
  }
}
```
特機事項はなし。。。
　

```dart:login.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'authentication_error.dart';
import 'registration.dart';
import '../home.dart';

class Login extends StatefulWidget {
  @override
  _LoginPage createState() => _LoginPage();
}

class _LoginPage extends State<Login> {

  String login_Email = "";  // 入力されたメールアドレス
  String login_Password = "";  // 入力されたパスワード
  String infoText = "";  // ログインに関する情報を表示

  // Firebase Authenticationを利用するためのインスタンス
  final FirebaseAuth auth = FirebaseAuth.instance;
  AuthResult result;
  FirebaseUser user;

  // エラーメッセージを日本語化するためのクラス
  final auth_error = Authentication_error();

 @override
 Widget build(BuildContext context) {

    return Scaffold(
        body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[

                // メールアドレスの入力フォーム
                Padding(
                  padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 0),
                  child:TextFormField(
                    decoration: InputDecoration(
                      labelText: "メールアドレス"
                    ),
                    onChanged: (String value) {
                      login_Email = value;
                    },
                  )
                ),

                // パスワードの入力フォーム
                Padding(
                  padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 10.0),
                  child:TextFormField(
                    decoration: InputDecoration(
                      labelText: "パスワード（8～20文字）"
                    ),
                    obscureText: true,  // パスワードが見えないようRにする
                    maxLength: 20,  // 入力可能な文字数
                    maxLengthEnforced: false,  // 入力可能な文字数の制限を超える場合の挙動の制御
                    onChanged: (String value) {
                      login_Password= value;
                    },
                  ),
                ),

                // ログイン失敗時のエラーメッセージ
                Padding(
                  padding: EdgeInsets.fromLTRB(20.0, 0, 20.0, 5.0),
                  child:Text(infoText,
                    style: TextStyle(color: Colors.red),),
                ),

                ButtonTheme(
                  minWidth: 350.0,
                  // height: 100.0,
                  child: RaisedButton(
                    child: Text('ログイン',
                      style: TextStyle(fontWeight: FontWeight.bold),),
                    textColor: Colors.white,
                    color: Colors.blue,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),

                    onPressed: () async {
                      try {
                        // メール/パスワードでユーザー登録
                        result = await auth.signInWithEmailAndPassword(
                          email: login_Email,
                          password: login_Password,
                        );

                        // ログイン成功
                        // ログインユーザーのIDを取得
                        user = result.user;
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => Home(user_id: user.uid),
                          )
                        );
                        
                      } catch (e) {
                        // ログインに失敗した場合
                        setState(() {
                          infoText = auth_error.login_error_msg(e.code);
                        });
                      }
                    }
                  ),
                ),
              ],
            ),
        ),

        // 画面下にボタンの配置
        bottomNavigationBar: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[

            Padding(
              padding: const EdgeInsets.all(8.0),
              child:ButtonTheme(
                minWidth: 350.0,
                // height: 100.0,
                child: RaisedButton(
                  child: Text('アカウントを作成する',
                  style: TextStyle(fontWeight: FontWeight.bold),),
                  textColor: Colors.blue,
                  color: Colors.blue[50],
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),

                  // ボタンクリック後にアカウント作成用の画面の遷移する。
                  onPressed: (){
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        fullscreenDialog: true,
                        builder: (BuildContext context) => Registration(),
                      ),
                    );
                  }

                ),
              ),
            ),
          ]),
    );
 }
} 
```
・効率的ではない気がしたけど、今回はログイン失敗時のメッセージ表示はStateの更新で対応。
・アカウントの作成ボタンは`bottomNavigationBar` に作成。
・エラーメッセージは、日本語文字を返してくれるクラス・メソッドを独自に作って利用。
　

```dart:registration.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'authentication_error.dart';
import '../home.dart';

// アカウント登録ページ
class Registration extends StatefulWidget {
  @override
  _RegistrationState createState() => _RegistrationState();
}

class _RegistrationState extends State<Registration> {

  String newEmail = "";  // 入力されたメールアドレス
  String newPassword = "";  // 入力されたパスワード
  String infoText = "";  // 登録に関する情報を表示
  bool pswd_OK;  // パスワードが有効な文字数を満たしているかどうか

  // Firebase Authenticationを利用するためのインスタンス
  final FirebaseAuth auth = FirebaseAuth.instance;
  AuthResult result;
  FirebaseUser user;

  // エラーメッセージを日本語化するためのクラス
  final auth_error = Authentication_error();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[

                Padding(
                  padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 30.0),
                  child:Text('新規アカウントの作成',
                    style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold))
                ),

                // メールアドレスの入力フォーム
                Padding(
                  padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 0),
                  child:TextFormField(
                    decoration: InputDecoration(labelText: "メールアドレス"),
                    onChanged: (String value) {
                      newEmail = value;
                    },
                  )
                ),

                // パスワードの入力フォーム
                Padding(
                  padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 10.0),
                  child:TextFormField(
                    decoration: InputDecoration(
                      labelText: "パスワード（8～20文字）"
                    ),
                    obscureText: true,  // パスワードが見えないようRにする
                    maxLength: 20,  // 入力可能な文字数
                    maxLengthEnforced: false,  // 入力可能な文字数の制限を超える場合の挙動の制御
                    onChanged: (String value) {
                      if(value.length >= 8){
                        newPassword= value;
                        pswd_OK = true;
                      }else{
                        pswd_OK = false;
                      }
                    }
                  ),
                ),

                // 登録失敗時のエラーメッセージ
                Padding(
                  padding: EdgeInsets.fromLTRB(20.0, 0, 20.0, 5.0),
                  child:Text(infoText,
                    style: TextStyle(color: Colors.red),),
                ),

                ButtonTheme(
                  minWidth: 350.0,
                  // height: 100.0,
                  child: RaisedButton(
                    child: Text('登録',
                      style: TextStyle(fontWeight: FontWeight.bold),),
                    textColor: Colors.white,
                    color: Colors.blue,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),

                    onPressed: () async {
                      if (pswd_OK){
                      try {
                        // メール/パスワードでユーザー登録
                        result = await auth.createUserWithEmailAndPassword(
                            email: newEmail,
                            password: newPassword,
                          );

                        // 登録成功
                        // 登録したユーザー情報
                        user = result.user;
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => Home(user_id: user.uid),
                          )
                        );

                      } catch (e) {
                        // 登録に失敗した場合
                        setState(() {
                          infoText = auth_error.register_error_msg(e.code);
                        });
                      }
                      }else{
                        setState(() {
                          infoText = 'パスワードは8文字以上です。';
                        });
                      }
                    },
                  ),
                ),
              ],
            ),
        ),
    );
  }
}
```
・`Firebase Authenticationの`パスワードポリシーはデフォルトで6文字以上となっているので、独自に8文字未満では登録できない様にしてみた。（しかもパスワードポリシー自体は、コンソールとかから簡単に変更できなそう・・・）
・こっちもアカウント作成失敗時のメッセージ表示はStateの更新で対応。
・エラーメッセージもログインと同様に、日本語文字を返してくれる様に独自クラスとメソッドを利用。
　


```dart:authentication_error.dart
// Firebase Authentication利用時の日本語エラーメッセージ
class Authentication_error {

  // ログイン時の日本語エラーメッセージ
  String login_error_msg(String error_code){

    String error_msg;

    if(error_code == 'ERROR_INVALID_EMAIL'){
      error_msg = '有効なメールアドレスを入力してください。';

    }else if (error_code == 'ERROR_USER_NOT_FOUND'){
      // 入力されたメールアドレスが登録されていない場合
      error_msg = 'メールアドレスかパスワードが間違っています。';

    }else if (error_code == 'ERROR_WRONG_PASSWORD'){
      // 入力されたパスワードが間違っている場合
      error_msg = 'メールアドレスかパスワードが間違っています。';

    }else if (error_code == 'error'){
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = 'メールアドレスとパスワードを入力してください。';

    }else{
      error_msg = error_code;
    }

    return error_msg; 
  }


  // アカウント登録時の日本語エラーメッセージ
  String register_error_msg(String error_code){

    String error_msg;

    if(error_code == 'ERROR_INVALID_EMAIL'){
      error_msg = '有効なメールアドレスを入力してください。';

    }else if (error_code == 'error'){
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = 'メールアドレスとパスワードを入力してください。';

    }else{
      error_msg = error_code;
    }

    return error_msg; 
  }

}
```
・エラーメッセージは多言語対応されていなそうだったので独自で作成。
　

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

// [Themelist] インスタンスにおける処理。
class Home extends StatelessWidget {

  final String user_id;
  Home({Key key, this.user_id}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body:Center(
        child:Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('ようこそ',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            Text(user_id),
          ],
        ),
      ),
    );
  }
}
```
特記事項なし。


#### 上記のコードのみで、一応は図で描いた様な動きにはなるので、もしよければ参考にしてください。
※コードをgitにアップロードしています。
　https://github.com/Smiler5617/flutter_functions/tree/master/login_func_FirebaesAuthentication/lib