FlutterでFirebaseを使用するサンプル - Authentication編

試した全機能を一つの記事にすると長くなりそうなので、機能ごとに分けたいと思います。
本記事は、`Firebase Authentication`についての記事です。
他の記事は以下に掲載しています
* Cloud Firesotre(Comming soon)
* Cloud Functions(Comming soon)
* Firebase Cloud Messaging(Comming soon)
* Cloud Storage(Comming soon)
* Cloud Analytics(Comming soon)

# はじめに
<!-- 発端や概要を記載 -->
`Flutter`で`Firebase`を使用する勉強をしたので、、
サンプルアプリ的な感じで試せるコードを書いていきたいと思います。
目次の上から順にサンプルコードをコピペ実装していただければ動作すると思います。

`Firebase`の環境構築と`Firebase`の各機能の環境構築方法については、この記事では書きません。
環境構築はして、ドキュメントを読みながら試そうと思ったけど、
サンプルアプリ的な感じで動作を試すのに、コード書くのめんどくさいなって方や、
ドキュメントで使い方の説明書いてあるけど、実際どう使えばいいかよくわからないと挫折した方向けになります。
UIとセットで簡単に`Firebase`の機能を試せるようにするというのが本記事の目的ですので、
勉強がてらさくっと試してみたい方に、コードのコピペで試せるように書いていきたいと思います。
またサンプルコードのポイントも解説していますので、ドキュメントで挫折した方の助けにもなるかと思います。

本記事では、`Firebase Authentication`の機能のうち以下を試せるサンプルコードを掲載します。
* ユーザー登録
* サインイン
* サインアウト
* ユーザー情報の編集

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [エントリポイント](#エントリポイント)
1. [認証状態によって、サインイン前のページとサインイン後のページを切り替える](#認証状態によって、サインイン前のページとサインイン後のページを切り替える)
1. [サインインとユーザー登録](#サインインとユーザー登録)
1. [ユーザー情報の取得とサインアウト](#ユーザー情報の取得とサインアウト)
1. [ユーザー情報の編集](#ユーザー情報の編集)

<!-- 各チャプター -->
<a id="#エントリポイント"></a>
# エントリポイント
サンプルアプリのエントリポイントは下記の実装にしています。
各機能の説明は、`MyHomePage`以下のコードを掲載しています。

```dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
```
<a id="#認証状態によって、サインイン前のページとサインイン後のページを切り替える"></a>
# 認証状態によって、サインイン前のページとサインイン後のページを切り替える


### 認証状態によって、サインイン前のページとサインイン後のページを切り替える
#### 【コードとポイント解説】
エントリポイントから最初に表示される画面である`MyHomePaga`は以下のコードです。
このコードにより、
**認証状態が未サインインの場合は、サインイン(とユーザー登録)画面**
**認証状態がサインイン済みの場合は、ユーザー情報を表示する画面**を表示しています。

```dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      //stream: FirebaseAuth.instance.authStateChanges(),
      stream: FirebaseAuth.instance.userChanges(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const SizedBox();
        }
        if (snapshot.hasData) {
          // Userデータがあるので、ユーザー情報表示画面へ
          return UserPage(user: snapshot.data);
        }
        // Userデータがないので、サインイン画面へ
        return const FirebaseSingInAndRegisterPage();
      },
    );
  }
}
```

**ポイントは、`StreamBuilder`を使用し、サインインの状態によって表示するページを切り替えている点**です。
**サインインの状態は、`FirebaseAuth.instance.userChanges()`で確認できます。**
これは`Stream`になるので、`StreamBuilder`の`stream`に指定しておいて、
**サインインの状態が変わったタイミングで適切なページを表示できる**ようにしています。
コメントアウトしていますが、**サインインの状態は`FirebaseAuth.instance.authStateChanges()`でも確認できます。**
**しかし、こちらはユーザーの情報編集時(名前やメールアドレスを変えたとき)に、通知が来ない**ため、
本サンプルでは`FirebaseAuth.instance.userChanges()`を使っています。

認証状態の取得の詳細については[こちら](https://firebase.google.com/docs/auth/flutter/start#auth-state)を参照ください

#### 【サインインの状態の保持について】
一度、サインインするとサインインの状態は保持されますので、アプリを再起動してもサインインしたままとなります。
サインインの状態はデバイスのキャッシュとして保持される仕組みのようです。
もし、キャッシュを消したい場合は、設定→アプリ一覧→該当アプリを選択し、キャッシュとデータを削除すれば、
再起動時、未サインインの状態となります。
※Androidのみ確認済みです。iPhoneは未確認です。あと、画面の遷移は大体なので実際と若干異なるかもです。
サインインの状態保持については[こちら](https://firebase.google.com/docs/auth/flutter/start?hl=ja#persisting_authentication_state)を参照

<a id="#サインインとユーザー登録"></a>
# サインインとユーザー登録
サインインとユーザー登録を行うページのサンプルコードは以下です。

```dart
class FirebaseSingInAndRegisterPage extends StatefulWidget {
  const FirebaseSingInAndRegisterPage({Key? key}) : super(key: key);

  @override
  State<FirebaseSingInAndRegisterPage> createState() => _FirebaseSingInAndRegisterPageState();
}

class _FirebaseSingInAndRegisterPageState extends State<FirebaseSingInAndRegisterPage> {
  bool _isStateSignIn = true;
  late TextEditingController _emailTextController;
  late TextEditingController _passwordTextController;

  @override
  void initState() {
    _emailTextController = TextEditingController();
    _passwordTextController = TextEditingController();
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Login"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8),
              child: TextField(
                controller: _emailTextController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter your email",
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8),
              child: TextField(
                controller: _passwordTextController,
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Enter your password",
                ),
              ),
            ),
            ElevatedButton(
              onPressed: () =>
              _isStateSignIn ? _onSingIn(context) : _onRegister(context),
              child: Text(
                _isStateSignIn ? "Sign in" : "Register",
              ),
            ),
            TextButton(
                onPressed: () {
                  setState(() {
                    _isStateSignIn = !_isStateSignIn;
                  });
                },
                child: Text(_isStateSignIn ? "Register?" : "Sing in?"))
          ],
        ),
      ),
    );
  }

  Future<void> _onSingIn(BuildContext context) async {
    try {
      final userCredential =
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: _emailTextController.text,
        password: _passwordTextController.text,
      );
    } catch (e) {
      _showErrorDialog(context, e.toString());
    }
  }

  Future<void> _onRegister(BuildContext context) async {
    try {
      final userCredential =
      await FirebaseAuth.instance.createUserWithEmailAndPassword(
        email: _emailTextController.text,
        password: _passwordTextController.text,
      );
    } catch (e) {
      _showErrorDialog(context, e.toString());
    }
  }

  void _showErrorDialog(BuildContext context, String error) {
    showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: const Text("Error"),
          content: Text(error),
          actions: <Widget>[
            TextButton(
              child: Text("OK"),
              onPressed: () => Navigator.pop(context),
            ),
          ],
        );
      },
    );
  }
}
```

ポイントを解説します。
サインインは、`_onSingIn`メソッドで実装しています。
サインインは、`FirebaseAuth.instance.signInWithEmailAndPassword`を呼ぶと実行されます。
その名の通り、Eメールとパスワードでサインインできます。
認証方法は他にもあるようですが、この記事では割愛します。
詳しくは公式のドキュメントを参照お願いします。

続いて、ユーザー登録は`_onRegister`メソッドで実装しています。
ユーザー登録は、`FirebaseAuth.instance.createUserWithEmailAndPassword`を呼ぶと実行されます。
その名の通り、Eメールとパスワードでユーザー登録します。
すでに登録済みのEメールを指定した場合エラーとなります。

<a id="#ユーザー情報の取得とサインアウト"></a>
# ユーザー情報の取得とサインアウト
ユーザー情報の取得とサインアウトを行うページのサンプルコードは以下です。
※コピペして動作させる場合、このコードだけだとエラーになるので、次の章のコードもコピーしてください。

```dart
class UserPage extends StatelessWidget {
  final User? user;
  const UserPage({Key? key, required this.user}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if(user != null){
      return Scaffold(
        appBar: AppBar(actions: [
          IconButton(
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (context) => const EditUserPage(),
                ),
              );
            },
            icon: const Icon(Icons.edit),
          ),
        ],),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(user?.displayName ?? "Dispaly name is none"),
              Text(user?.email ?? "Email is none"),
              ElevatedButton(
                onPressed: () => FirebaseAuth.instance.signOut(),
                child: const Text("Sign out"),
              )
            ],
          ),
        ),
      );
    }

    return const Scaffold(
      body: Center(
        child: Text("No User"),
      ),
    );
  }
}
```

ポイントを解説します。
### 【ユーザー情報の取得】
ユーザーの情報は、`User`クラスに入っています。
`User`クラスには表示名やEメール、電話番号など設定情報を取得できます。
具体的には`user?.displayName`、`user?.email`で取得可能です。
ユーザー登録した時点では表示名や電話番号などは未設定でnullとなっています。

サインインしている`User`は、 [認証状態によって、サインイン前のページとサインイン後のページを切り替える](#認証状態によって、サインイン前のページとサインイン後のページを切り替える)で説明した`FirebaseAuth.instance.authStateChanges()`と`FirebaseAuth.instance.userChanges()`の`Stream`で取得できます。
また、`Stream`を使わない方法であれば、`FirebaseAuth.instance.currentUser`でも取得できます。

### 【サインアウト】
サインアウトは`FirebaseAuth.instance.signOut()`でできます。
サンプルコードは雑に書いて例外処理してませんが、未サインインの状態だと例外出るかもしれません。
(本サンプルコードでは、未サインインの状態でこのページに遷移することはないので心配ありませんが、ちゃんと書くなら例外処理はした方が良いです)

<a id="#ユーザー情報の編集"></a>
# ユーザー情報の編集
```dart
class _EditUserPageState extends State<EditUserPage> {
  late TextEditingController _displayNameController;
  late TextEditingController _emailController;

  @override
  void initState() {
    final currentUser = FirebaseAuth.instance.currentUser;
    final initDisplayName = currentUser?.displayName ?? "";
    final initEmail = currentUser?.email ?? "";
    _displayNameController = TextEditingController(text: initDisplayName);
    _emailController  = TextEditingController(text: initEmail);
    super.initState();
  }

  @override
  void dispose() {
    _displayNameController.dispose();
    _emailController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Edit user"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                controller: _displayNameController,
                decoration: const InputDecoration(
                  hintText: "Enter your display name",
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                controller: _emailController,
                decoration: const InputDecoration(
                  hintText: "Enter your email",
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            ElevatedButton(
              onPressed: () => _save(context),
              child: const Text("Save"),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _save(BuildContext context) async {
    final currentUser = FirebaseAuth.instance.currentUser;
    if (currentUser == null) {
      _showErrorDialog(context, "Current user is null.");
    }

    try {
      final currentDisplayName = currentUser?.displayName ?? "";
      if (currentDisplayName != _displayNameController.text) {
        await currentUser?.updateDisplayName(_displayNameController.text);
      }

      final currentEmail = currentUser?.email ?? "";
      if (currentEmail != _emailController.text) {
        await currentUser?.updateEmail(_emailController.text);
      }

      if (!mounted) return;
      Navigator.of(context).pop();
    } catch (e) {
      _showErrorDialog(context, e.toString());
    }
  }

  void _showErrorDialog(BuildContext context, String error) {
    showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: const Text("Error"),
          content: Text(error),
          actions: <Widget>[
            TextButton(
              child: Text("OK"),
              onPressed: () => Navigator.pop(context),
            ),
          ],
        );
      },
    );
  }
}
```

ポイントを解説します。
### 【ユーザー情報の編集】
ユーザー情報は`_save`メソッド内で実装しています。
具体的には`await currentUser?.updateDisplayName(_displayNameController.text);`と
`await currentUser?.updateEmail(_emailController.text);`です。
情報の編集は、`User`クラスの`update～`というメソッドで編集可能です。
情報を編集すると、`FirebaseAuth.instance.userChanges()`で通知されるので、
編集後の情報の`User`が取得でき、UserPageが再描画されます。

# さいごに
Authenticationについては以上です。
最初、公式ドキュメントで勉強したとき、サインイン(ユーザー登録) → 情報表示 → サインアウトという一連の流れが、
コードとしてまとまってなかったので、とっかかりにくい印象を受けました。
ただ今回の記事でまとめてみたので、最初の一歩として参考になれば幸いです。