【Flutter】FirebaseAuth Google認証の方法

##  初めに
現在開発中のアプリに Firebase のメール、パスワード認証以外の認証方法を取り入れようと思い、 Google 認証を実装したので、その方法を共有します。

## 記事の対象者
+ Flutter と Firebase の基礎をある程度理解している方 ( 基礎については解説しないため )
+ Firebase を用いた Google 認証を実装したい方
+ ユーザ認証方法を追加してアプリをより便利にしたい方



## 準備
### パッケージの追加
まずは以下の三つのパッケージを「 pubspeck.yaml 」に記述します。
パッケージのバージョンは、特に制約がなければ最新のバージョンで問題ありません。

+ [firebase_core](https://pub.dev/packages/firebase_core)
+ [firebase_auth](https://pub.dev/packages/firebase_auth)
+ [google_sign_in パッケージ](https://pub.dev/packages/google_sign_in)

```yaml: pubspeck.yaml
dependencies:
  flutter:
    sdk: flutter

　　　　firebase_core: ^1.21.0
　　　　firebase_auth: ^3.6.4
  google_sign_in: ^5.4.1
```
Pub get をしてパッケージの準備は完了です。
<br>

次は Firebase 側での設定です。
今回は Flutter のプロジェクトに Firebase を追加した状態を前提としています。
Flutter に Firebase を追加する手順は[こちら](https://firebase.google.com/docs/flutter/setup?platform=ios)をご参照ください。

### Google 認証の有効化
Flutter に Firebase を追加した後、 Firebase のコンソールへ移動し、 
Authentication > Sign-in method > 新しいプロバイダを追加 で以下の画像のように Google によるサインインを追加してください。
![スクリーンショット 2022-08-21 21.40.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/b428eb50-793d-bf75-5a6e-c96bd46cb244.png)

Google によるサインインが有効になると、以下のように「 Google 」の項目が有効になります。
![スクリーンショット 2022-08-21 21.40.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/ce5d9e85-4a74-51a4-ba26-32cc6cb59be3.png)

Android 端末で Google 認証をするための準備は以上で完了です。
しかし、 iOS 端末で Google 認証をするためにはもう少し準備が必要です。

すべきことは以下の通りです。
+ Runner ディレクトリに GoogleService-Info.plist を追加
+ GoogleService-Info.plist の REVERSED_CLIENT_ID を Info.plist に追加

それぞれ順を追って解説します。

### GoogleService-Info.plist を追加
まずは Firebase のコンソールを開きます。
![スクリーンショット 2022-08-21 22.01.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/dea7aa59-8ebc-83ab-ce7e-472d1fd3a127.png)
<br>

次に青字の「プロジェクトの概要」の隣の歯車のアイコンを押して、「プロジェクトの設定」を選択します。
![スクリーンショット 2022-08-21 22.02.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/28c4c43f-3166-287c-0c22-d862fbe010d6.png)

「プロジェクトの設定」の「全般」のページの一番下に「マイアプリ」という部分があり、そこに以下の画像ようなボタンがあるので押下します。
![スクリーンショット 2022-08-21 22.21.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/314f35fd-890f-2f3f-8843-d04913910c07.png)
すると GoogleService-Info.plist がダウンロードされます。

※ Android アプリの方は 「 google-service.json 」 というファイルがダウンロードされますが、これとは異なるので注意しましょう。
<br>

次にダウンロードした GoogleService-Info.plist を 以下のディレクトリに配置します。
プロジェクト名 > ios > Runner > GoogleService-Info.plist

一度 flutter run しましょう。

### REVERSED_CLIENT_ID を Info.plist に追加
次に GoogleService-Info.plist と同じディレクトリにある「 Info.plist 」を開いて、 `<dict>`タグの中に以下のコードを記述しましょう。
```Info.plist
<!-- Google Sign-In section -->
<key>CFBundleURLTypes</key>
<array>
	<dict>
		<key>CFBundleTypeRole</key>
		<string>Editor</string>
		<key>CFBundleURLSchemes</key>
		<array>
			<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
			<string> ここに REVERSED_CLIENT_ID をペースト !! </string>
		</array>
	</dict>
</array>
<!-- End of the Google Sign-In section -->
```
<br>

次に GoogleService-Info.plist を見てみましょう。
```GoogleService-Info.plist
<key>CLIENT_ID</key>
<string>YOUR_CLIENT_ID</string>
<key>REVERSED_CLIENT_ID</key>
<string> ここが REVERSED_CLIENT_ID </string>
<key>API_KEY</key>
<string>YOUR_API_KEY</string>
```
`<key>REVERSED_CLIENT_ID</key>` の下にある、 string タグの中にある文字列が REVERSED_CLIENT_ID です。

この REVERSED_CLIENT_ID をコピーして、再び「 Info.plist 」を開き、「 ここに REVERSED_CLIENT_ID をペースト !! 」と書かれている部分に REVERSED_CLIENT_ID をペーストしましょう。

最後に flutter run を実行して、エラーが出なければ準備は完了です。

Flutter 3.0 のアップデート以前までは 「 GoogleService-Info.plist 」を Runnner ディレクトリに配置するという手順が Firebase の導入の際に組み込まれていましたが、アップデート後はその手順が省略されているため、Google 認証を実装する際には一手間かかるようになっています。

## 実装
準備が完了したので、コードの実装に取り掛かりましょう！

まずは Firebase を使えるように「 main.dart 」の内容を変更しましょう。
```dart: main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
```
これで Firebase の初期化が完了し、Firebase が使用可能になります。
※ 以下の記事で紹介している通り、コードを間違えたり、記述が足りなかったりすると正常に作動しなくなるので注意しましょう。

https://qiita.com/KKoichi51/items/5e79ce996aca90c12b29

<br>
<br>

Google 認証の完成イメージは以下の通りです。
![スクリーンショット 2022-08-22 10.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/7846d107-d2f2-60b4-c289-913403e58282.png)
home_screen はアイコンとテキストの簡素なページで、画面をタップすると google_sign_in_screen へ遷移します。

google_sign_in_screen には Google 認証を行うためのボタンを設置し、ボタンを押して、アカウントを選択すると user_info_screen へ遷移します。

user_info_screen には サインインしたユーザの User Name、 User Email を表示させます。
また、 Sign Out ボタンを押すとサインアウトして home_screen に遷移するようにします。
<br>

今回は以下の Widget を作成します。
+ HomeScreen
+ GoogleSignInButton
+ GoogleSignInScreen
+ UserInfoScreen

まずは Authentication に関する処理をまとめた Authentication クラスを作成しましょう。
<br>

## Authentication
コードは以下の通りです。
```dart: authentication.dart
class Authentication {
  // Firebase initialization
  static Future<FirebaseApp> initializeFirebase(
      {required BuildContext context}) async {
    FirebaseApp firebaseApp = await Firebase.initializeApp();

    User? user = FirebaseAuth.instance.currentUser;

    if (user != null) {
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(
          builder: (context) => UserInfoScreen(
            user: user,
          ),
        ),
      );
    }
    return firebaseApp;
  }

  // GoogleSignIn
  static Future<User?> signInWithGoogle({required BuildContext context}) async {
    FirebaseAuth auth = FirebaseAuth.instance;
    User? user;

    final GoogleSignIn googleSignIn = GoogleSignIn();

    final GoogleSignInAccount? googleSignInAccount =
        await googleSignIn.signIn();

    if (googleSignInAccount != null) {
      final GoogleSignInAuthentication googleSignInAuthentication =
          await googleSignInAccount.authentication;

      final AuthCredential credential = GoogleAuthProvider.credential(
        accessToken: googleSignInAuthentication.accessToken,
        idToken: googleSignInAuthentication.idToken,
      );

      try {
        final UserCredential userCredential =
            await auth.signInWithCredential(credential);

        user = userCredential.user;
      } on FirebaseAuthException catch (e) {
        if (e.code == 'account-exists-with-different-credential') {
          ScaffoldMessenger.of(context).showSnackBar(
            Authentication.customSnackBar(
              content:
                  'The account already exists with a different credential.',
            ),
          );
        } else if (e.code == 'invalid-credential') {
          ScaffoldMessenger.of(context).showSnackBar(
            Authentication.customSnackBar(
              content: 'Error occurred while accessing credentials. Try again.',
            ),
          );
        }
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          Authentication.customSnackBar(
            content: 'Error occurred using Google Sign-In. Try again.',
          ),
        );
      }
    }
    return user;
  }

  // GoogleSignOut
  static Future<void> signOut({required BuildContext context}) async {
    final GoogleSignIn googleSignIn = GoogleSignIn();

    try {
      if (!kIsWeb) {
        await googleSignIn.signOut();
      }
      await FirebaseAuth.instance.signOut();
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        Authentication.customSnackBar(
          content: 'Error signing out. Try again.',
        ),
      );
    }
  }

  //ErrorSnackBar
  static SnackBar customSnackBar({required String content}) {
    return SnackBar(
      backgroundColor: Colors.black,
      content: Text(
        content,
        style: const TextStyle(color: Colors.redAccent, letterSpacing: 0.5),
      ),
    );
  }
}
```

冗長見えるかもしれませんが、クラス内で行っている処理は以下の三つです。
+ Firebase の初期化
+ Google Sign In
+ Google Sign Out
+ エラー時の SnackBar の指定

### Firebase の初期化
```dart: authentication.dart
static Future<FirebaseApp> initializeFirebase(
      {required BuildContext context}) async {
    FirebaseApp firebaseApp = await Firebase.initializeApp();

    User? user = FirebaseAuth.instance.currentUser;

    if (user != null) {
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(
          builder: (context) => UserInfoScreen(
            user: user,
          ),
        ),
      );
    }
    return firebaseApp;
  }
```
このコードでは Firebase の初期化を行なっています。
以下の画像の赤い矢印の処理の際、Firebase を初期化し、ユーザがいるかどうかを確認します。
ユーザがいる場合は UserInfoScreen へ遷移します。
![スクリーンショット 2022-08-23 10.06.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/2d376ec7-b5b3-500b-8791-762dbb4efab9.png)
<br>

### Google Sign In
```dart: authentication.dart
// GoogleSignIn
  static Future<User?> signInWithGoogle({required BuildContext context}) async {
    FirebaseAuth auth = FirebaseAuth.instance;
    User? user;

    final GoogleSignIn googleSignIn = GoogleSignIn();

    final GoogleSignInAccount? googleSignInAccount =
        await googleSignIn.signIn();

    if (googleSignInAccount != null) {
      final GoogleSignInAuthentication googleSignInAuthentication =
          await googleSignInAccount.authentication;

      final AuthCredential credential = GoogleAuthProvider.credential(
        accessToken: googleSignInAuthentication.accessToken,
        idToken: googleSignInAuthentication.idToken,
      );

      try {
        final UserCredential userCredential =
            await auth.signInWithCredential(credential);

        user = userCredential.user;
      } on FirebaseAuthException catch (e) {
        if (e.code == 'account-exists-with-different-credential') {
          // handle the error here
          ScaffoldMessenger.of(context).showSnackBar(
            Authentication.customSnackBar(
              content:
                  'The account already exists with a different credential.',
            ),
          );
        } else if (e.code == 'invalid-credential') {
          // handle the error here
          ScaffoldMessenger.of(context).showSnackBar(
            Authentication.customSnackBar(
              content: 'Error occurred while accessing credentials. Try again.',
            ),
          );
        }
      } catch (e) {
        // handle the error here
        ScaffoldMessenger.of(context).showSnackBar(
          Authentication.customSnackBar(
            content: 'Error occurred using Google Sign-In. Try again.',
          ),
        );
      }
    }
    return user;
  }
```
このコードでは Google 認証によるサインインを行なっています。
より詳しく解説します。

```dart: authentication.dart
FirebaseAuth auth = FirebaseAuth.instance;
User? user;

final GoogleSignIn googleSignIn = GoogleSignIn();

final GoogleSignInAccount? googleSignInAccount =
  await googleSignIn.signIn();
```
このコードでは Authentication クラスの中で使用頻度の高いメソッドをまとめて変数に代入しています。 Authentication に限らず、処理が複雑、冗長になる場合はこのように変数にまとめるのが基本と言えます。
<br>

```dart: authentication.dart
  if (googleSignInAccount != null) {
      final GoogleSignInAuthentication googleSignInAuthentication =
          await googleSignInAccount.authentication;

      final AuthCredential credential = GoogleAuthProvider.credential(
        accessToken: googleSignInAuthentication.accessToken,
        idToken: googleSignInAuthentication.idToken,
      );
```
このコードでは if 文による条件分岐で、 googleSignInAccount が null ではない場合の処理を記述しています。この条件下でアカウントが保持している `accessToken` `idToken` を取得しています。
変数 `credential` の意味が「資格証明」であるように、これらのトークンは Google 認証の際の証明書のような働きをします。
<br>

```dart: authentication.dart
  try {
        final UserCredential userCredential =
            await auth.signInWithCredential(credential);

        user = userCredential.user;
      } on FirebaseAuthException catch (e) {
        if (e.code == 'account-exists-with-different-credential') {
          ScaffoldMessenger.of(context).showSnackBar(
            Authentication.customSnackBar(
              content:
                  'The account already exists with a different credential.',
            ),
          );
        } else if (e.code == 'invalid-credential') {
          ScaffoldMessenger.of(context).showSnackBar(
            Authentication.customSnackBar(
              content: 'Error occurred while accessing credentials. Try again.',
            ),
          );
        }
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          Authentication.customSnackBar(
            content: 'Error occurred using Google Sign-In. Try again.',
          ),
        );
      }
    }
    return user;
  }
```
このコードでは try / catch 構文を使って認証とエラーハンドリングを行なっています。

先ほど取得した `credential` を `signInWithCredential()` メソッドに引数として渡すことで、 Google 認証が完了し、`user` が取得できます。

エラーは以下の三つの場合に分けて、それぞれ別のエラーを表示しています。
+ `credential` で渡されたトークンが既に存在している場合
+ `credential`　へのアクセスに失敗した場合
+ 上記以外、つまり Firebase 以外の Google 認証でエラーが出た場合

### Google Sign Out
```dart: authentication.dart
// Google Sign Out
  static Future<void> signOut({required BuildContext context}) async {
    final GoogleSignIn googleSignIn = GoogleSignIn();

    try {
      if (!kIsWeb) {
        await googleSignIn.signOut();
      }
      await FirebaseAuth.instance.signOut();
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        Authentication.customSnackBar(
          content: 'Error signing out. Try again.',
        ),
      );
    }
  }
```
このコードではサインアウトを行なっています。
`kIsWeb` とは、flutter foundation パッケージに用意されている変数で、アプリケーションが Web 上で実行するようにコンパイルされているかどうかを調べるための変数です。

アプリケーションが Web 上で実行するようにコンパイルされている場合は true、そうでない場合は false になります。
したがって、上のコードでは Web 上ではない場合は Google 認証の `signOut()` メソッドを使用し、 Web 上の場合は FirebaseAuth の `signOut() ` メソッドを使用しています。

また、エラーが出た場合は SnackBar を出して警告するようにしています。

### エラー時の SnackBar の指定
```dart: authentication.dart
  //ErrorSnackBar
  static SnackBar customSnackBar({required String content}) {
    return SnackBar(
      backgroundColor: Colors.black,
      content: Text(
        content,
        style: const TextStyle(color: Colors.redAccent, letterSpacing: 0.5),
      ),
    );
  }
```
このコードではエラー時の SnackBar の指定をしています。
引数として `content` を受け取っていますが、これはエラーの内容を表す文字列です。
<br>

## HomeScreen
次に HomeScreen を作成します。
全体コードは以下の通りです。
```dart: home_screen.dart
// HomeScreen
class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (context) => const GoogleSignInScreen()));
        },
        child: Expanded(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: const <Widget>[
                Icon(
                  Icons.home,
                  size: 70,
                ),
                Text(
                  "Home",
                  style: TextStyle(fontSize: 25),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
```
以下の画像のように、アイコンとテキストだけのシンプルなページです。
<img width=250, src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/c65e0fe3-ae9d-96c9-fd77-0fc0e6afbbfc.png">

GestureDetector を使って、アイコンとテキストをタップすると google_sign_in_screen に遷移するよう指定してあります。
<br>

## GoogleSignInButton
次に GoogleSignInButton です。
コンポーネントとして管理するためのスクリーンとは切り離します。
全体コードは以下の通りです。
```dart: google_sign_in_button.dart
final isSignInProvider = StateProvider<bool>((ref) => false);

class GoogleSignInButton extends ConsumerWidget {
  const GoogleSignInButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isSignIn = ref.watch(isSignInProvider.notifier);

    return Padding(
      padding: const EdgeInsets.only(bottom: 16.0),
      child: isSignIn.state
          ? const CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            )
          : OutlinedButton(
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all(Colors.white),
                shape: MaterialStateProperty.all(
                  RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(40),
                  ),
                ),
              ),
              onPressed: () async {
                isSignIn.state = true;

                User? user =
                    await Authentication.signInWithGoogle(context: context);

                isSignIn.state = false;

                if (user != null) {
                  Navigator.of(context).pushReplacement(
                    MaterialPageRoute(
                      builder: (context) => UserInfoScreen(
                        user: user,
                      ),
                    ),
                  );
                }
              },
              child: Padding(
                padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: const <Widget>[
                    Image(
                      image: AssetImage("images/google_logo.png"),
                      height: 35.0,
                    ),
                    Padding(
                      padding: EdgeInsets.only(left: 10),
                      child: Text(
                        'Googleでサインイン',
                        style: TextStyle(
                          fontSize: 20,
                          color: Colors.black54,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                    )
                  ],
                ),
              ),
            ),
    );
  }
}

```
見た目は以下のようになります。
<img width=250, src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/1747ae8d-5453-671f-0bd3-33f45f34e4e6.png">
コードの内容について以下で詳しく解説します。
<br>
```dart: google_sign_in_button.dart
final isSignInProvider = StateProvider<bool>((ref) => false);
```
まずこのコードで、サインインの状態を監視するための StateProvider を作成しています。
ボタンが押されてから Google認証によるサインインの処理が発火するため、初期値は false になっています。
<br>


```dart: google_sign_in_button.dart
final isSignIn = ref.watch(isSignInProvider.notifier);
```
このコードでは、先ほど作成した isSignInProvider の状態を読み取る notifier を `isSignIn` という変数に代入し、ConsumerWidget 内で使えるようにしています。
<br>

```dart: google_sign_in_button.dart
return Padding(
      padding: const EdgeInsets.only(bottom: 16.0),
      child: isSignIn.state
          ? const CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            )
```
このコードでは Padding を返し、その子要素を `isSignIn.state` の値によって変化するようにしています。
`isSignIn.state` は先述した notifier の state ( 状態 ) であるため、 boolean 型になります。

条件分岐は三項演算子によって行われています。
三項演算子とは以下のような条件式のことです。
```markdown
​(条件) ? trueの場合 : falseの場合
```

したがって、上のコードは isSignIn.state が true の場合 `CircularProgressIndicator()` を表示させるという意味になります。

もう少し噛み砕くと、ユーザがサインインしている状態、またはサインインを試みている状態ではサインインボタンを表示する必要がないため、`CircularProgressIndicator()` を表示させているということです。
<br>

```dart: google_sign_in_button.dart
: OutlinedButton(
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all(Colors.white),
                shape: MaterialStateProperty.all(
                  RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(40),
                  ),
                ),
              ),
              onPressed: () async {
                isSignIn.state = true;

                User? user =
                    await Authentication.signInWithGoogle(context: context);

                isSignIn.state = false;

                if (user != null) {
                  Navigator.of(context).pushReplacement(
                    MaterialPageRoute(
                      builder: (context) => UserInfoScreen(
                        user: user,
                      ),
                    ),
                  );
                }
              },
```
このコードでは先述した三項演算子による条件分岐の false の場合、つまり、ユーザがサインしていなかった場合についての処理を記述しています。

`isSignIn` が false の場合は `OutlinedButton` を表示させており、その onPressed の処理で Google 認証によるサインインを行なっています。

また、サインイン前後で `isSignIn.state` の切り替えを行うことで、表示を変更しています。

そして、処理の最後で条件分岐を行い、 `user` が null ではない、つまり、`user` が取得できた時点で次の画面へ遷移するようにしています。

※ なお、以下のコードでは [Authentication クラス](#google-sign-in) からメソッドを呼び出しています。
```dart: google_sign_in_button.dart
User? user = await Authentication.signInWithGoogle(context: context);
```
<br>

GoogleSignInButton を表示させて、押した際に以下の画像のように Firebase の Authentication の Users に表示されれば、正常に作動しています。
![スクリーンショット 2022-08-31 17.49.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/e8917b4b-f0f1-efae-331b-9934d0924760.png)
Google 認証でユーザ登録した場合は、メールアドレス、パスワードによる認証とは異なり、Google のアイコンが表示されます。
<br>


## GoogleSignInScreen
次に GoogleSignInScreen を作成します。
全体コードは以下の通りです。
```dart: google_signin_screen.dart
class GoogleSignInScreen extends StatelessWidget {
  const GoogleSignInScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.only(
            left: 16.0,
            right: 16.0,
            bottom: 20.0,
          ),
          child: Column(
            mainAxisSize: MainAxisSize.max,
            children: [
              Row(),
              Expanded(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: const [
                    Flexible(
                      flex: 1,
                      child: Image(
                        image: AssetImage('images/google_logo.png'),
                        height: 160,
                      ),
                    ),
                    SizedBox(height: 260),
                    Text(
                      'Google',
                      style: TextStyle(
                        fontSize: 40,
                      ),
                    ),
                    Text(
                      'Authentication',
                      style: TextStyle(
                        fontSize: 40,
                      ),
                    ),
                  ],
                ),
              ),
              FutureBuilder(
                future: Authentication.initializeFirebase(context: context),
                builder: (context, snapshot) {
                  if (snapshot.hasError) {
                    return const Text('Error initializing Firebase');
                  } else if (snapshot.connectionState == ConnectionState.done) {
                    return const GoogleSignInButton();
                  }
                  return const CircularProgressIndicator(
                    valueColor: AlwaysStoppedAnimation<Color>(
                      Colors.orange,
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```
見た目は以下のようにロゴとテキスト、そして GoogleSignInButton からなるページです。
<img width=250, src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/c2fe5883-7a63-80b8-002b-b50947d88c6d.png">

UI 構築のためのコードは基礎的な Widget の組み合わせであるため、説明は省略します。
<br>

```dart: google_signin_screen.dart
FutureBuilder(
  future: Authentication.initializeFirebase(context: context),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return const Text('Error initializing Firebase');
    } else if (snapshot.connectionState == ConnectionState.done) {
      return const GoogleSignInButton();
    }
    return const CircularProgressIndicator(
      valueColor: AlwaysStoppedAnimation<Color>(
        Colors.orange,
      ),
    );
  },
),
```
このコードではまず Firebase の初期化を行い、その後以下の三つの条件分岐を行なっています。
+ Firebase から出された snapshot にエラーが含まれていた場合
+ Firebase との接続が可能になった場合
+ それ以外の場合

### Firebase から出された snapshot にエラーが含まれていた場合
Firebase から出力される snapshot にはエラーの有無を判定するための hasError という値が存在し、エラーがある場合は true、ない場合は false になります。
この値を使うことでエラーがある場合とそうでない場合で変化を与えることができます。

したがってこのコードで、エラーがある場合にテキストでそれを伝えるようにしています。
```dart: google_signin_screen.dart
if (snapshot.hasError) {
  return const Text('Error initializing Firebase');
}
```
<br>

### Firebase との接続が可能になった場合
Firebase から出力される snapshot には connectionState という値もあり、この値は Firebase との通信の接続状況を表すための値です。
通信が可能な場合は `(snapshot.connectionState == ConnectionState.done)`
この条件文が true になります。

したがって、以下のコードは Firebase との通信が可能になったときには GoogleSignInButton を表示させるという処理になります。
```dart: google_signin_screen.dart
else if (snapshot.connectionState == ConnectionState.done) {
  return const GoogleSignInButton();
}
```
<br>

※ 補足ではありますが、`snapshot.connectionState` はデータ取得の際にも使用されます。
```dart
if (snapshot.connectionState == ConnectionState.waiting) {
  return CircularProgressIndicator(),
}
```
このコードで 「Firebase からデータを取得している最中である」という状態を表すことができ、データ取得の最中は `CircularProgressIndicator()` を表示させることでユーザに待ってもらうことができます。
<br>

### それ以外の場合
それ以外の場合とは 「 Firebase からエラーが出力されず、Firebase との接続が完了していない状態 」であり、つまり、Firebase が正常に作動し、接続を待っている状態と言えます。
この場合、ユーザを待たせているため、`CircularProgressIndicator()` を表示させるのが適切と言えるでしょう。
```dart: google_signin_screen.dart
return const CircularProgressIndicator(
  valueColor: AlwaysStoppedAnimation<Color>(
    Colors.orange,
  ),
);
```
<br>


## UserInfoScreen
最後に UserInfoScreen です。
全体コードは以下の通りです。
```dart:user_info_screen.dart
class UserInfoScreen extends StatefulWidget {
  const UserInfoScreen({Key? key, required User user})
      : _user = user,
        super(key: key);

  final User _user;

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

class _UserInfoScreenState extends State<UserInfoScreen> {
  late User _user;
  bool _isSigningOut = false;

  Route _routeToSignInScreen() {
    return PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) =>
          const HomeScreen(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        var begin = const Offset(-1.0, 0.0);
        var end = Offset.zero;
        var curve = Curves.ease;

        var tween =
            Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

        return SlideTransition(
          position: animation.drive(tween),
          child: child,
        );
      },
    );
  }

  @override
  void initState() {
    _user = widget._user;

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        title: const Text("SecondPage"),
      ),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.only(
            left: 16.0,
            right: 16.0,
            bottom: 20.0,
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              _user.photoURL != null
                  ? ClipOval(
                      child: Material(
                        child: Image.network(
                          _user.photoURL!,
                          fit: BoxFit.fitHeight,
                        ),
                      ),
                    )
                  : const ClipOval(
                      child: Material(
                        child: Padding(
                          padding: EdgeInsets.all(16.0),
                          child: Icon(
                            Icons.person,
                            size: 60,
                          ),
                        ),
                      ),
                    ),
              const SizedBox(height: 16.0),
              const Text(
                'ようこそ',
                style: TextStyle(
                  fontSize: 26,
                ),
              ),
              const SizedBox(height: 8.0),
              Text(
                _user.displayName!,
                style: const TextStyle(
                  fontSize: 26,
                ),
              ),
              const SizedBox(height: 8.0),
              Text(
                '( ${_user.email!} )',
                style: const TextStyle(
                  fontSize: 20,
                  letterSpacing: 0.5,
                ),
              ),
              const SizedBox(height: 24.0),
              const Text(
                "Google アカウントを使用してサインインしています。\n サインアウトするには以下のボタンを押してください。",
                style: TextStyle(
                    fontSize: 14,
                    letterSpacing: 0.2),
              ),
              const SizedBox(height: 16.0),
              _isSigningOut
                  ? const CircularProgressIndicator(
                      valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
                    )
                  : ElevatedButton(
                      style: ButtonStyle(
                        backgroundColor: MaterialStateProperty.all(
                          Colors.redAccent,
                        ),
                        shape: MaterialStateProperty.all(
                          RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(10),
                          ),
                        ),
                      ),
                      onPressed: () async {
                        setState(() {
                          _isSigningOut = true;
                        });
                        await Authentication.signOut(context: context);
                        setState(() {
                          _isSigningOut = false;
                        });
                        Navigator.of(context)
                            .pushReplacement(_routeToSignInScreen());
                      },
                      child: const Padding(
                        padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
                        child: Text(
                          'Sign Out',
                          style: TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                            color: Colors.white,
                            letterSpacing: 2,
                          ),
                        ),
                      ),
                    ),
            ],
          ),
        ),
      ),
    );
  }
}
```

見た目は以下のようになります。
今回はモザイクを設けていますが、ユーザの名前とメールアドレスが表示されます。
<img width=250, src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609901/08653a0a-ce00-cabf-4583-ef3c220748a5.png">

長いコードですが、それぞれ詳細を解説していきます。

```dart:user_info_screen.dart
class UserInfoScreen extends StatefulWidget {
  const UserInfoScreen({Key? key, required User user})
      : _user = user,
        super(key: key);

  final User _user;

  @override
  _UserInfoScreenState createState() => _UserInfoScreenState();
}
```
まずこのコードで `Uer` を引数として受け取り、`_user` という変数に代入しています。
この画面でサインインしているユーザのアイコンの画像とメールアドレスを表示させるため、 `User` を引数として受け取ることを必須としています。
<br>

```dart:user_info_screen.dart
  late User _user;
  bool _isSigningOut = false;
```
このコードで、 StatefulWidget で受け取った `User` を State の方で使えるようにしています。
また、ユーザがサインアウトしているかどうかを判断するために `_isSignOut` という変数を設けています。
<br>

```dart:user_info_screen.dart
  void initState() {
    _user = widget._user;

    super.initState();
  }
```
このコードで初期状態を設定し、StatefulWidget から受け取った `User` を `_user` という変数に代入し、初期化しています。
<br>

```dart:user_info_screen.dart
_user.photoURL != null
  ? ClipOval(
    child: Material(
      child: Image.network(
        _user.photoURL!,
        fit: BoxFit.fitHeight,
      ),
    ),
  )
  : const ClipOval(
    child: Material(
      child: Padding(
        padding: EdgeInsets.all(16.0),
        child: Icon(
          Icons.person,
          size: 60,
        ),
      ),
    ),
  ),
```
このコードでは `_user.photoURL` が null ではないとき、つまり、ユーザの画像がある場合は、その URL で指定されている画像を表示させ、画像がない場合はアイコンを表示させる処理をしています。
<br>

```dart:user_info_screen.dart
Text(
  _user.displayName!,
  style: const TextStyle(
    fontSize: 26,
  ),
),
const SizedBox(height: 8.0),
Text(
  '( ${_user.email!} )',
  style: const TextStyle(
    fontSize: 20,
    letterSpacing: 0.5,
  ),
),
```
このコードでは先程の画像と同様に `_user` の名前とメールアドレスを表示させています。
ただし、アカウントは必ず名前とメールアドレスを持っているため、null を許容しないコードになっています。
<br>

```dart:user_info_screen.dart
 _isSigningOut
  ? const CircularProgressIndicator(
    valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
  )
  : ElevatedButton(
    style: ButtonStyle(
      backgroundColor: MaterialStateProperty.all(
        Colors.redAccent,
      ),
      shape: MaterialStateProperty.all(
        RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
      ),
    ),
    onPressed: () async {
      setState(() {
        _isSigningOut = true;
      });
      await Authentication.signOut(context: context);
      setState(() {
        _isSigningOut = false;
      });
      Navigator.of(context).pushReplacement(_routeToSignInScreen());
    },
    child: const Padding(
      padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
      child: Text(
        'Sign Out',
        style: TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
          color: Colors.white,
          letterSpacing: 2,
        ),
      ),
    ),
),
```
このコードでは `_isSigningOut` の値に応じて表示させる Widget を変化させています。
`_isSigningOut` が true の場合は `CircularProgressIndicator()` を表示させています。

false の場合、つまり、ユーザがサインインした状態では `ElevatedButton()` を表示させて、その onPressed でサインアウトする処理を記述しています。
以下が onPressed の処理です。
```dart:user_info_screen.dart
onPressed: () async {
  setState(() {
    _isSigningOut = true;
  });
  await Authentication.signOut(context: context);
  setState(() {
    _isSigningOut = false;
  });
  Navigator.of(context).pushReplacement(_routeToSignInScreen());
},
```
`await Authentication.signOut(context: context);`
この部分がサインアウトにあたりますが、サインインの際と同様にサインアウト前後に `_isSigningOut` の値を切り替えています。こうすることで、ユーザがサインアウトボタンを押した際に `CircularProgressIndicator()` が表示されるようになります。

そして、サインアウトが完了したのち、HomeScreen へ遷移するようになっています。

以上です。

## あとがき
今回は非常に長い内容でしたが、最後まで読んでいただきありがとうございました。

参考にしていただければ幸いです。
誤っている箇所があればご指摘いただければ幸いです。

### 参考にしたサイト

https://blog.codemagic.io/firebase-authentication-google-sign-in-using-flutter-jpn/

https://pub.dev/packages/google_sign_in