Flutter と Firebase Auth を用いたアプリで、起動時にログイン状態に応じたページを表示する方法

Flutter と Firebase を使用したアプリを開発する際に、Firebase Auth を使用することは多いと考えられますが、本記事では、アプリの起動時に Firebase Auth のログイン状態に応じたページを返す方法についてまとめます。

達成したい状況は、

- アプリの起動時（使用中）にユーザーの Firebase Auth のログイン状態を確認・監視する
- そのログイン状態に応じて、未ログイン時のサインイン画面、ログイン済み時のホーム画面などを出し分ける

ということです。

そのような方法を学ぶ前には、次のようなコードを実装しているかもしれません。

```dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Flutter app',
        home: SignInPage(),
}

class SignInPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => const Scaffold(
        body: Center(
          child: Text('未サインイン時に表示するサインイン画面です。'),
        ),
      );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => const Scaffold(
        body: Center(
          child: Text('サインイン済み時に表示するホーム画面です。'),
        ),
      );
}
```

このようにすると、ユーザーのサインイン状態にかかわらず、毎回 `MaterialApp` の `home` 属性に指定された `SignInPage` が表示されることになってしまいます。

これを解決し、冒頭の達成したい状況を満たすための最も簡単な方法の一つは、 `FirebaseAuth.instance` の `authStateChanges()` メソッドと `StreamBuilder` を使用することです。

公式の [FlutterFire のドキュメント](https://firebase.flutter.dev/docs/auth/usage/#authentication-state]https://firebase.flutter.dev/docs/auth/usage/#authentication-state) にも記述があります。

> Firebase Auth enables you to subscribe in realtime to this state via a Stream. Once called, the stream provides an immediate event of the user's current authentication state, and then provides subsequent events whenever the authentication state changes. To subscribe to these changes, call the authStateChanges() method on your FirebaseAuth instance:
> 

と説明されている通り、`authStateChanges()` メソッドによって、FirebaseAuth インスタンスのログイン状態の変更を監視 (subscribe) することができ、それを `Stream` 型でリアルタイムに受け取ることができます。

つまり、次のようなソースコードを記述すれば完成です。

```dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Flutter app',
        home: StreamBuilder<User?>(
          stream: FirebaseAuth.instance.authStateChanges(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              // スプラッシュ画面などに書き換えても良い
              return const SizedBox();
            }
            if (snapshot.hasData) {
              // User が null でなない、つまりサインイン済みのホーム画面へ
              return HomePage();
            }
            // User が null である、つまり未サインインのサインイン画面へ
            return SignInPage();
          },
        ),
      );
}

class SignInPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => const Scaffold(
        body: Center(
          child: Text('未サインイン時に表示するサインイン画面です。'),
        ),
      );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => const Scaffold(
        body: Center(
          child: Text('サインイン済み時に表示するホーム画面です。'),
        ),
      );
}
```

`StreamBuilder` の `stream` 属性には、Firebase Auth の `authStateChanges()` メソッドを指定しており、 `Stream` で Firebase Auth の `User?` 型を返します。つまり、サインイン済みであることが確認されればそのユーザーを、そうでなければ `null` を返すということです。

`AsyncSnapshot<User?>` 型の `snapshot` の `connectionState` や `snapshot.data` の内容に応じてホーム画面とサインイン画面を出し分ければ、目的通りの実装が完成します。