Flutterやってみたよ part2 （FirebaseAuthを使ってみます編）

# はじめに
[前回：Flutterやってみたよ part1 （導入編）](https://qiita.com/190yamashita/items/6d867c11921bf0658531)に続いてFlutterとFirebaseの連携をやってみようと思います。
Firebaseは無料で使える機能が充実していますので、個人的には積極的に使っていきたいです〜
（どこぞやに買収とかされないことを祈っております）

今回は@nunnally_engr_0114さんの[FirebaseとFlutterでアプリ開発してみる【其ノ二：Flutter＆Dart篇】](https://qiita.com/nunnally_engr_0114/items/bcaca94b4e8ef75cdddf)
を参考にさせてもらいました。勉強になりました、本当にありがとうございますm(_ _)m

# 目的
- FirebaseAuthを利用してサインイン・サインアウト機能を有した画面を作る
- まだアーキテクチャは導入しない（次回にやりたい）

今回で画面を作ってみて画面遷移方法・状態管理方法など学べたらいいなと思います。
そこまでいったらアークテクチャの導入をやってみようと思います。

# 画面構成
まずは画面を準備しようと思います。構成はシンプルに

- スプラッシュスクリーン
- サインイン
- サインアップ
- ホーム

でいこうと思います。こんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325497/1b415b04-3280-18ea-5e0e-09e972cb8989.png)


# 画面実装
　flutterのライフサイクルとかの基礎は他の記事を参考にということで割愛します。
　以下が参考になりました。

 　参考
 　[公式サイト](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html#performance-considerations)
 　[Flutter の Widget ツリーの裏側で起こっていること](https://medium.com/flutter-jp/dive-into-flutter-4add38741d07)
 　[Stateful Widget のパフォーマンスを考慮した正しい扱い方](https://medium.com/flutter-jp/state-performance-7a5f67d62edd)

## パッケージの導入
使用するパッケージを導入します。
必要になったら都度追加でOKです。今回は作ってから記事を書いてますのでまとめて

pubspec.yaml

```dart
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2

  firebase_core: ^0.3.0
  firebase_auth: ^0.9.0
  firebase_messaging: ^4.0.0

  cloud_firestore: ^0.9.0
  cloud_functions: ^0.1.0

  progress_dialog: ^1.2.0

  intl: ^0.15.7
```

## 画像の追加
画像を追加する場合もpubspec.yamlに宣言を追加して見えるようにする必要があります。
今回はassetsの直下にsplash.pngをおきました。

```dart
flutter:
  assets:
    - assets/splash.png
```


## スプラッシュスクリーン

```dart

//　ここがコンストラクタみたいなもの。引数を追加して初期化するところです。
//　今回はauthを引数に渡して初期化しています
class SplashScreen extends StatefulWidget {
  SplashScreen({Key key, this.auth}) : super(key: key);
  final BaseAuth auth;

  // ここが画面を構成するところ。privateな_SplashScreenStateを呼び出す形にしてます
  @override
  State<StatefulWidget> createState() => _SplashScreenState();
}

// これが画面を構成するクラス
class _SplashScreenState extends State<SplashScreen> {

  AuthStatus _authStatus = AuthStatus.notSignedIn;

  // 初期処理で認証してタイマーを起動
  @override
  void initState() {
    super.initState();

    // Firebase 認証
    widget.auth.currentUser().then((userId) {
      _authStatus = userId != null ? AuthStatus.signedIn : AuthStatus.notSignedIn;
    });

    _startTimer();
  }

  // buildが画面を構成しているところです。
  // 今回は画面にスプラッシュ画像をセットしてます
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Image(image: AssetImage('assets/splash.png')),
        ),
      ),
    );
  }

  // ３秒後に認証ずみならホーム、未認証ならサインインに遷移します
  _startTimer() async {
    Timer(Duration(seconds: 3), () {
      switch (_authStatus) {
        case AuthStatus.notSignedIn:
          Navigator.pushReplacement(context, MaterialPageRoute(
              builder: (context) => new SignInScreen(title: 'SignIn', auth: widget.auth)
          ));
          break;
        case AuthStatus.signedUp:
          Navigator.push(context, MaterialPageRoute(
              builder: (context) => new SignUpScreen(title: 'SignUp', auth: widget.auth)
          ));
          break;
        case AuthStatus.signedIn:
          Navigator.pushReplacement(context, MaterialPageRoute(
              builder: (context) => new HomeScreen(title: 'Home', auth: widget.auth)
          ));
          break;
      }
    });
  }

}
```

## サインイン
サインインのページです。

```dart
class SignInScreen extends StatefulWidget {
  SignInScreen({Key key, this.title, this.auth}) : super(key: key);
  final String title;  // タイトル
  final BaseAuth auth;  // 認証状態

  @override
  _SignInScreenState createState() => _SignInScreenState();

}

class _SignInScreenState extends State<SignInScreen> {

  static final formKey = new GlobalKey<FormState>();

  User _user = new User();
  String _authHint = '';
  
  // これは画面のバリデーションチェックでエラーになっているか判定します。
  bool validateAndSave() {
    final form = formKey.currentState;

    // widgetにはバリデーション機能が備わっているので画面上のバリデーションをform単位で検証することができます。
    if (form.validate()) {
      // OKだったらsaveすることでバリデーションの状態を保存できます
      form.save();
      return true;
    }
    return false;
  }

  // サインインボタンをタップした時のイベント
  void validateAndSubmit(BuildContext context) async {

    setState(() {
      _authHint = '';
    });

    if (validateAndSave()) {
      // グルグルを使用（package:progress_dialogを使用）
      final ProgressDialog progress = new ProgressDialog(context);
      try {
        // グルグルを表示。
        progress.show();
        // awaitで実行が終わるまで待ちます。
        await widget.auth.signIn(_user.email, _user.password);
        // グルグル閉じてから遷移する
        if (progress.isShowing()) {
          progress.hide();
          progress.dismiss();
        }
        // ホームに遷移
        Navigator.pushReplacement(context, MaterialPageRoute(
            builder: (context) => new HomeScreen(title: 'Home', auth: widget.auth)
        ));
      } catch (e) {
        // なんかエラーがでたら画面に表示します
        setState(() {
          _authHint = 'サインインでエラーが発生しました。\n\n${e.toString()}';
        });
        print(e);
      } finally {
        // グルグルを閉じる
        if (progress.isShowing()) {
          progress.hide();
          progress.dismiss();
        }
      }
    }

  }

  // サインアップボタンのイベントです
  void signUpSubmit() async {
    setState(() {
      _authHint = '';
    });
    // サインアップに遷移
    Navigator.push(context, MaterialPageRoute(
        builder: (context) => new SignUpScreen(title: 'SignUp', auth: widget.auth)
    ));
  }

  // ヒントエリアのコンポーネント生成メソッド
  Widget hintText() {
    return new Container(
        padding: const EdgeInsets.all(32.0),
        child: new Text(
            _authHint,
            key: new Key('hint'),
            style: new TextStyle(fontSize: 18.0, color: Colors.grey),
            textAlign: TextAlign.center
        ),
    );
  }

  // paddingのコンポーネント生成メソッド
  Widget padded({Widget child}) {
    return new Padding(
      padding: EdgeInsets.symmetric(vertical: 8.0),
      child: child,
    );
  }

  // 入力テキストエリアのコンポーネント生成メソッド
  List<Widget> usernameAndPassword() {
    return [
      padded(child: new TextFormField(
        key: new Key('email'),
        decoration: new InputDecoration(labelText: 'Email'),
        autocorrect: false,
        validator: (val) => val.isEmpty ? 'emailを入力してください' : null,
        onSaved: (val) => _user.email = val,
      )),
      padded(child: new TextFormField(
        key: new Key('password'),
        decoration: new InputDecoration(labelText: 'Password'),
        obscureText: true,
        autocorrect: false,
        validator: (val) => val.isEmpty ? 'パスワードを入力してください' : null,
        onSaved: (val) => _user.password = val,
      )),
    ];
  }

  // ボタンエリアのコンポーネント生成メソッド
  List<Widget> submitWidgets(BuildContext context) {
    return [
      new PrimaryButton(
          key: new Key('signIn'),
          text: 'サインイン',
          height: 44.0,
          onPressed: () => validateAndSubmit(context)
      ),
      new FlatButton(
          key: new Key('need-account'),
          textColor: Colors.green,
          child: new Text(
              "初めて利用する方\n（サインアップ）",
              textAlign: TextAlign.center
          ),
          onPressed: signUpSubmit
      ),
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login Screen'),
      ),
      body: new Center(
        child: new Form(
          child: new SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 16.0),
            child: new Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                // それっぽく画像を表示
                const SizedBox(
                  height: 200.0,
                  width: 200.0,
                  child: Image(
                    image: AssetImage('assets/splash.png'),
                    fit: BoxFit.fill,
                  ),
                ),
                hintText(),
                const SizedBox(height: 10.0),
                new Center(
                    child: new Form(
                        key: formKey,
                        child: new Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: usernameAndPassword() + submitWidgets(context),
                        )
                    )
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```

## サインアップ

```dart
class SignUpScreen extends StatefulWidget {
  SignUpScreen({Key key, this.title, this.auth}) : super(key: key);
  final String title;
  final BaseAuth auth;

  @override
  _SignUpState createState() => new _SignUpState();
}

class _SignUpState extends State<SignUpScreen> {
  static final formKey = new GlobalKey<FormState>();

  User _user = new User();
  String _authHint = '';

  bool isValidate() {
    final form = formKey.currentState;
    if (form.validate()) {
      form.save();
      return true;
    }
    return false;
  }

  // サインインタップ時のメソッド
  void onPressSignIn() async {
    setState(() {
      _authHint = '';
    });
    // 前の画面に戻る
    Navigator.pop(context);
  }

  // サインアップップ時のメソッド
  void onPressSignUp() async {

    setState(() {
      _authHint = '';
    });

    if (isValidate()) {
      // グルグルを使用（package:progress_dialogを使用）
      final ProgressDialog progress = new ProgressDialog(context);
      try {
        // グルグルを表示
        progress.show();
        // FirebaseAuthでユーザ作成
        await widget.auth.createUser(_user.email, _user.password, _user.displayName);
        // 成功したらそのままサインイン
        await widget.auth.signIn(_user.email, _user.password);
        // グルグルと閉じてから遷移
        if (progress.isShowing()) {
          progress.hide();
          progress.dismiss();
        }
        // ホームへ遷移
        Navigator.pushReplacement(context, MaterialPageRoute(
            builder: (context) => new HomeScreen(title: 'Home', auth: widget.auth)
        ));
      } catch (e) {
        // なんかエラー出たらヒントに表示
        setState(() {
          _authHint = 'サインインでエラーが発生しました。\n\n${e.toString()}';
        });
        print(e);
      } finally {
        // グルグルを閉じる
        if (progress.isShowing()) {
          progress.hide();
          progress.dismiss();
        }
      }
    }

  }

  // 入力エリアコンポーネント生成メソッド
  List<Widget> inputWidgets() {
    return [
      padded(child: new TextFormField(
        key: new Key('displayName'),
        decoration: new InputDecoration(labelText: 'Name'),
        autocorrect: false,
        validator: (val) => val.isEmpty ? '表示名を入力してください' : null,
        onSaved: (val) => _user.displayName = val,
      )),
      padded(child: new TextFormField(
        key: new Key('email'),
        decoration: new InputDecoration(labelText: 'Email'),
        autocorrect: false,
        validator: (val) => val.isEmpty ? 'emailを入力してください' : null,
        onSaved: (val) => _user.email = val,
      )),
      padded(child: new TextFormField(
        key: new Key('password'),
        decoration: new InputDecoration(labelText: 'Password'),
        obscureText: true,
        autocorrect: false,
        validator: (val) => val.isEmpty ? 'パスワードを入力してください' : null,
        onSaved: (val) => _user.password = val,
      )),
    ];
  }

  // ボタンコンポーネント生成メソッド
  List<Widget> submitWidgets() {
    return [
      new PrimaryButton(
        key: new Key('register'),
        text: 'サインアップ',
        height: 44.0,
        onPressed: onPressSignUp
      ),
      new FlatButton(
        key: new Key('login'),
        textColor: Colors.green,
        child: new Text(
            "既にアカウントをお持ちの方\n（サインイン）",
            textAlign: TextAlign.center
        ),
        onPressed: onPressSignIn
      ),
    ];
  }
  
  // ヒントエリアのコンポーネント生成メソッド
  Widget hintText() {
    return new Container(
        //height: 80.0,
        padding: const EdgeInsets.all(32.0),
        child: new Text(
            _authHint,
            key: new Key('hint'),
            style: new TextStyle(fontSize: 18.0, color: Colors.grey),
            textAlign: TextAlign.center)
    );
  }

  // paddingのコンポーネント生成メソッド
  Widget padded({Widget child}) {
    return new Padding(
      padding: EdgeInsets.symmetric(vertical: 8.0),
      child: child,
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      backgroundColor: Colors.grey[300],
      body: new SingleChildScrollView(child: new Container(
        padding: const EdgeInsets.all(16.0),
        child: new Column(
          children: [
            hintText(),
            new Card(
              child: new Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                new Container(
                    padding: const EdgeInsets.all(16.0),
                    child: new Form(
                        key: formKey,
                        child: new Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: inputWidgets() + submitWidgets(),
                        )
                    )
                ),
              ])
            ),
          ]
        )
      ))
    );
  }

```

## ホーム
ホームはドロワーだけ作って中身はありません、、、すみませんmm

```dart
class HomeScreen extends StatefulWidget {
  HomeScreen({Key key, this.title, this.auth, this.onSignOut}) : super(key: key);
  final String title;
  final BaseAuth auth;
  final VoidCallback onSignOut;

  @override
  _HomeScreenState createState() => _HomeScreenState();

}

class _HomeScreenState extends State<HomeScreen> {

  // サインアウトを実行してサインインへ遷移
  void onSignOut() {

    try {
      widget.auth.signOut();
      Navigator.pushReplacement(context, MaterialPageRoute(
          builder: (context) => new SignInScreen(title: 'SignIn', auth: widget.auth)
      ));
    } catch (e) {
      // トーストでエラー表示してみた
      Fluttertoast.showToast(
          msg: 'サインアウトでエラーが発生しました。\n\n${e.toString()}',
          toastLength: Toast.LENGTH_SHORT,
          gravity: ToastGravity.CENTER,
          timeInSecForIos: 1,
          backgroundColor: Colors.red,
          textColor: Colors.white,
          fontSize: 12.0
      );
    }

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Text('Home'),
      ),
      drawer: new Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: <Widget>[
            DrawerHeader(
              child: Text('メニュー'),
              decoration: BoxDecoration(
                image: DecorationImage(
                  image: AssetImage('assets/splash.png'),
                  fit: BoxFit.fill,
                  alignment: Alignment.center,
                ),
              ),
            ),
            ListTile(
              title: Text('サインアウト'),
              onTap: onSignOut,
            ),
          ],
        ),
      ),
    );
  }
}

```

## Firebaseのapi作成
今回はemail、パスワードを使って認証するようになってます。
メッセージも使いたいなと思ってたのでとりあえずトークンも紐づけてます。

ここで見慣れない **Future** というものが出てきましたので軽く説明します。
FutureとはDartで非同期処理を書く時の標準のクラスです。
swiftやkotlinでの非同期処理（Rx）やったことあるならすんなり理解できると思います。
アーキテクチャを導入すればRxdartを使用することになると思います。

参考
[Future＜T＞ class](https://api.dartlang.org/stable/2.7.0/dart-async/Future-class.html)
[Dartでの非同期処理（then vs async/await）](https://qiita.com/akatsukaha/items/f7c4fa9746e69f9b458d)

```dart
abstract class BaseAuth {
  Future<String> signIn(String email, String password);
  Future<void> signOut();
  Future<String> createUser(String email, String password, String displayName);
  Future<String> updateUser(String email, String password, String displayName);
  Future<String> currentUser();
}

enum AuthStatus {
  notSignedIn,
  signedIn,
  signedUp
}

class Auth implements BaseAuth {

  final FirebaseAuth _firebaseAuth;
  final FirebaseMessaging _firebaseMessaging;
  final BaseUtil _util = new Util();

  Auth({FirebaseAuth firebaseAuth, FirebaseMessaging firebaseMessaging})
      : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
        _firebaseMessaging = firebaseMessaging ?? new FirebaseMessaging()
  ;

  Future<String> signIn(String email, String password) async {

    FirebaseUser user = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);

    // deviceToken取得
    String token = await _firebaseMessaging.getToken();

    // Usersテーブル更新
    var db = Firestore.instance;
    await db.collection("users").document(user.uid).updateData({
      "deviceToken": token,
      "signInAt": _util.nowDatetimeString()
    });

    return user.uid;
  }

  Future<void> signOut() async {
    return _firebaseAuth.signOut();
  }

  Future<String> createUser(String email, String password, String displayName) async {

    FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);

    // Firebase UserInfo 更新
    UserUpdateInfo info = new UserUpdateInfo();
    info.displayName = displayName; // 表示名前
    user.updateProfile(info);

    // deviceToken取得
    String _token = await _firebaseMessaging.getToken();

    // Usersテーブル作成
    var db = Firestore.instance;
    await db.collection("users").document(user.uid).setData({
      "deviceToken": _token,
      "displayName": displayName,
      "createdAt": _util.nowDatetimeString(),
      "updatedAt": _util.nowDatetimeString(),
      "deletedAt": ''
    });

    return user.uid;
  }

  Future<String> updateUser(String email, String password, String displayName) async {

    FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);

    // Firebase UserInfo 更新
    UserUpdateInfo info = new UserUpdateInfo();
    info.displayName = displayName; // 表示名前
    user.updateProfile(info);

    // deviceToken取得
    String _token = await _firebaseMessaging.getToken();

    // Usersテーブル作成
    var db = Firestore.instance;
    await db.collection("users").document(user.uid).updateData({
      "deviceToken": _token,
      "displayName": displayName,
      "updatedAt": _util.nowDatetimeString()
    });

    return user.uid;
  }

  Future<String> currentUser() async {
    FirebaseUser user = await _firebaseAuth.currentUser();
    return user != null ? user.uid : null;
  }

}
```

# ソース

現時点（2019/12/26）でのソース公開します。随時更新していきます❗️
[flutter_firebase_sample](https://github.com/ikuo-yamashita/flutter_firebase_sample)

# 終わりに
なんとなくflutterでアプリが作れそうな気がしてきましたwww
ただこのままじゃお仕事で使えないので次回は[アーキテクチャ調査編](https://qiita.com/190yamashita/items/f327cf66de1a65ff3a2f)です。
ではまた👋

# 参考
大変勉強になりました！ありがとうございました🙇‍♂️
- [FirebaseとFlutterでアプリ開発してみる【其ノ二：Flutter＆Dart篇】](https://qiita.com/nunnally_engr_0114/items/bcaca94b4e8ef75cdddf)
- [Flutter の Widget ツリーの裏側で起こっていること](https://medium.com/flutter-jp/dive-into-flutter-4add38741d07)
- [Stateful Widget のパフォーマンスを考慮した正しい扱い方](https://medium.com/flutter-jp/state-performance-7a5f67d62edd)
- [Dartでの非同期処理（then vs async/await）](https://qiita.com/akatsukaha/items/f7c4fa9746e69f9b458d)