🍇

Apple × Google × メール認証機能をFlutter × Supabase × Riverpodで実装する

2024/10/05に公開

この記事では、FlutterとSupabaseを活用して、Google、Apple、メール認証を実装する方法を紹介します。アーキテクチャにはRiverpodを採用し、状態管理も効率的に行います。Resultを用いてエラーハンドリングの簡略化も行っております。

前提

この記事をスムーズに進めるためには、以下の前提条件を満たしていることが推奨されます。

  • Flutterの基本的な知識があること
  • Supabaseのアカウントがあり、プロジェクトが作成されていること
  • Riverpodを利用した状態管理の基本を理解していること
  • Google、AppleのOAuth設定がGoogle CloudやApple Developerアカウントで完了していること

この記事では、まず初めにコード全体を貼っており、そこからそのコードの詳細な説明をしていくというスタイルでやっております!

では、早速始めましょう!

1.認証のためのライブラリを導入

最初に、supabase_flutterやriverpodなど必要なパッケージをpubspec.yamlに追加します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  supabase_flutter: ^2.7.0
  flutter_riverpod: ^2.5.1
  freezed_annotation: ^2.4.4
  json_serializable: ^6.8.0
  google_sign_in: ^6.2.1
  sign_in_with_apple: ^6.1.3

これらのパッケージを使って、Google、Apple、メール認証の機能を実装していきます。

2.ディレクトリ構成

Flutterアプリケーションでは、ディレクトリ構造を整理することがメンテナンスや拡張性を高めるために非常に重要です。ここでは、Supabase、Riverpod、Freezedを使用した認証機能を中心にしたディレクトリ構成を提案します。

ディレクトリ構成

lib/
├── main.dart                 # エントリーポイント
├── services/                 # 外部APIとの連携
│   └── supabase_service.dart # Supabaseとの認証ロジック
├── repositories/             # データ操作を管理
│   └── auth_repository.dart  # 認証リポジトリ
├── state/                    # 状態管理
│   └── auth_state.dart       # 認証状態を管理
├── models/                   # データモデル
│   ├──result.dart            # Freezedを利用した結果モデル
│   └──result.freezed.dart    #自動生成されるファイル
├── views/                    # UIレイヤー
│   ├── user_screen.dart      #ユーザー情報表示画面                  
│   └── signup_screen.dart     # サインアップ画面
└── core/                     # 共通の型定義やロジック
    └── typedefs/             # 型定義
        └── result_typedef.dart # 成功・失敗結果の型定義

各ディレクトリの役割

  • lib/main.dart: アプリケーションのエントリーポイント。runApp()からアプリの起動が開始されます。
  • services/: Supabaseなど外部のAPIと連携するロジックを管理。今回はsupabase_service.dartに、Google、Apple、メール認証のロジックを実装しています。
  • repositories/: 認証リポジトリを通じて、サービス層から取得したデータをアプリケーションに提供します。(ここでResultの型を使用します。)
  • state/: Riverpodを使った状態管理を担当。認証状態を管理し、UIとデータの橋渡しを行います。
  • models/: Freezedを使ったデータモデルを管理します。結果やエラー処理をモデル化し、可読性を高めています。build_runnerコマンドでresult.freezed.dartが自動生成されます。
  • views/: UI層を管理します。今回はsignup_screen.dartにサインアップ画面のUIを実装しています。
  • core/typedefs/: 汎用的に再利用できる型定義を格納します。成功・失敗を管理するResult型の定義が含まれています。

3.ResultとFutureResultを使ってエラー処理をシンプルにする

Flutterアプリにおいて非同期処理の結果を柔軟に扱えるようにするためのResultクラスと、それに関連するFutureResultの定義について紹介します。Resultを使うと、成功・失敗の結果を一貫して扱えるようになり、エラーハンドリングがとても簡単になります。

Resultクラスの実装

まずは、Resultクラスを見ていきましょう。このクラスは、freezedパッケージを使って実装されています。freezedを使うと、データクラスや状態管理に役立つ不変クラス(immutable class)を簡単に作成できます。

result.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'result.freezed.dart';


class Result<T> with _$Result<T> {
  const factory Result.success(T value) = Success<T>;
  const factory Result.failure() = Failure<T>;
}
  • Resultはジェネリクス<T>を使って、どんな型のデータにも対応できる汎用的なクラスです。
  • Resultには、成功時のsuccessと失敗時のfailureの2つの状態があります。successには成功時のデータが含まれ、failureは何も返さないシンプルなエラー状態を表します。
  • freezedによって、このクラスのコードが自動生成され、状態管理やデータ操作がとても簡単になります。

自動生成の準備が完了したので自動生成のコマンドを打ちましょう!

flutter run build_runner build

保存する度に監視してほしい場合はbuildをwatch にしましょう。

flutter run build_runner watch

例えば、APIコールの結果として成功したユーザー情報を扱いたい場合は、以下のように使います。

final result = await _authRepository.signInWithGoogle();

result.when(
  success: (user) {
    print("サインアップ成功: $user");
  },
  failure: () {
    print("サインアップ失敗");
  },
);

これで、API呼び出しが成功した場合にはユーザー情報が取得でき、失敗した場合にはエラーメッセージを表示するなどの処理を行えます。

FutureResultの定義

次に、FutureResultについて説明します。これはResultをFutureでラップした型エイリアス(typedef)で、非同期処理の結果をResultでラップして扱います。

result_typedef.dart
import 'package:{アプリ名}/models/result/result.dart';

typedef FutureResult<T> = Future<Result<T>>;
  • FutureResult<T>は、Futureが完了した時にResult<T>を返す構造です。これにより、非同期処理で成功・失敗をきちんと扱うことができます。
  • 非同期処理が行われるAPI呼び出しやデータベース操作で、これを利用することでエラーハンドリングが統一されます。

例えば、以下のように使います。

FutureResult<User?> signInWithGoogle() async {
  try {
    final res = await _supabaseService.signInWithGoogle();
    return Result.success(res);
  } catch (e) {
    return Result.failure();
  }
}

ここで、API呼び出しが成功すればResult.successを返し、失敗した場合にはResult.failureを返すという構造になります。

4.SupabaseServiceの実装

まず、Supabaseを使用してGoogle、Apple、メール認証を行うサービス層を作成します。

SupabaseServiceコード全体

以下がsupabase_service.dartの全体のコードになります。

supabase_service.dart
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class SupabaseService {
  final SupabaseClient _client = Supabase.instance.client;
  final GoogleSignIn _googleSignIn = GoogleSignIn();

  // Google認証
  Future<User?> signInWithGoogle() async {
    try {
      final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
      if (googleUser == null) return null;

      final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
      final AuthResponse res = await _client.auth.signInWithIdToken(
        provider: OAuthProvider.google,
        idToken: googleAuth.idToken!,
        accessToken: googleAuth.accessToken,
      );
      return res.user;
    } catch (error) {
      print('Error signing in with Google: $error');
      return null;
    }
  }

  // Apple認証
  Future<User?> signInWithApple() async {
    try {
      final credential = await SignInWithApple.getAppleIDCredential(
        scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName],
      );

      final AuthResponse res = await _client.auth.signInWithIdToken(
        provider: OAuthProvider.apple,
        idToken: credential.identityToken!,
        accessToken: credential.authorizationCode,
      );
      return res.user;
    } catch (error) {
      print('Error signing in with Apple: $error');
      return null;
    }
  }

  // メールアドレスでサインアップ
  Future<User?> signUpWithEmail(String email, String password) async {
    final AuthResponse response = await _client.auth.signUp(
      email: email,
      password: password,
    );
    return response.user;
  }

  // サインアウト
  Future<void> signOut() async {
    await _client.auth.signOut();
  }

  // 現在のユーザーを取得
  User? getCurrentUser() {
    return _client.auth.currentUser;
  }
}

Google認証

まずは、Google認証から見ていきます。

supabase_service.dart
Future<User?> signInWithGoogle() async {
  try {
    final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
    if (googleUser == null) return null;

    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
    final AuthResponse res = await _client.auth.signInWithIdToken(
      provider: OAuthProvider.google,
      idToken: googleAuth.idToken!,
      accessToken: googleAuth.accessToken,
    );
    return res.user;
  } catch (error) {
    print('Error signing in with Google: $error');
    return null;
  }
}
  • GoogleSignInを使ってユーザーがGoogleアカウントでサインアップすると、googleUserが取得できます。
  • googleUser.authenticationを呼び出して、idTokenとaccessTokenを取得。これらをSupabaseのsignInWithIdTokenで使ってサインアップを完了させます。

このフローを使うことで、ユーザーはGoogleの認証画面を通じてサインアップでき、認証情報がSupabaseで管理されます。

Apple認証

次に、Apple認証です。Appleユーザー向けのサインアップも簡単に実装できます。

supabase_service.dart
Future<User?> signInWithApple() async {
  try {
    final credential = await SignInWithApple.getAppleIDCredential(
      scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName],
    );

    final AuthResponse res = await _client.auth.signInWithIdToken(
      provider: OAuthProvider.apple,
      idToken: credential.identityToken!,
      accessToken: credential.authorizationCode,
    );
    return res.user;
  } catch (error) {
    print('Error signing in with Apple: $error');
    return null;
  }
}
  • SignInWithAppleを使って、Appleの認証情報(idTokenなど)を取得します。
  • そのトークンを使って、Supabase側でAppleサインアップを処理します。

Apple認証は主にiOS向けですが、Apple IDでのログインをサポートしているアプリなら必須の機能ですね。

メール認証

そして、古典的だけど多くのアプリでまだまだ使われているメール認証も対応。

supabase_service.dart
Future<User?> signUpWithEmail(String email, String password) async {
  final AuthResponse response = await _client.auth.signUp(
    email: email,
    password: password,
  );
  return response.user;
}
  • signUpWithEmailメソッドを使って、メールアドレスとパスワードでサインアップします。
  • シンプルに、メールとパスワードを引数にして呼び出すだけでOKです。

メール認証はユーザーにとって安心感があり、簡単に実装できるので、多くのアプリで使われています。

サインアウト機能

サインインができたら、次はサインアウトも実装しておきましょう。

supabase_service.dart
Future<void> signOut() async {
  await _client.auth.signOut();
}

こちらも超簡単。signOut() を呼び出すだけで、サインアウトが完了します。

現在のユーザー情報を取得

最後に、アプリで今サインインしているユーザーを取得する方法です。

supabase_service.dart
User? getCurrentUser() {
  return _client.auth.currentUser;
}

これを使えば、今ログインしているユーザー情報を取得して、アプリ内で表示したり、必要な処理に活用できます。

5.AuthRepositoryの実装

このファイルでは、Supabaseとのやりとりを担当するsupabase_service.dartを利用して、認証に関する操作をすべて管理しています。リポジトリ層を作ることで、UIやビジネスロジックと認証ロジックの依存を減らし、変更や拡張がしやすい構造にしています。

AuthRepositoryコード全体

以下がauth_repository.dartの全体のコードになります。

auth_repository.dart
import 'package:{アプリ名}/core/typedefs/result_typedef.dart';
import 'package:{アプリ名}/models/result/result.dart';
import '../services/supabase_service.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

class AuthRepository {
  final SupabaseService _supabaseService = SupabaseService();

  FutureResult<User?> signInWithGoogle() async {
    try {
      final res = await _supabaseService.signInWithGoogle();
      return Result.success(res);
    } catch (e) {
      return Result.failure();
    }
  }

  FutureResult<User?> signInWithApple() async {
    try {
      final res = await _supabaseService.signInWithApple();
      return Result.success(res);
    } catch (e) {
      return Result.failure();
    }
  }

  FutureResult<User?> signUpWithEmail(String email, String password) async {
    try {
      final res = await _supabaseService.signUpWithEmail(email, password);
      return Result.success(res);
    } catch (e) {
      return Result.failure();
    }
  }

  FutureResult<void> signOut() async {
    try {
      await _supabaseService.signOut();
      return Result.success(null);
    } catch (e) {
      return Result.failure();
    }
  }

  FutureResult<User?> getCurrentUser() async {
    try {
      final res = await _supabaseService.getCurrentUser();
      return Result.success(res);
    } catch (e) {
      return Result.failure();
    }
  }
}

では、具体的な認証処理を見ていきましょう。

Google認証の実装

auth_repository.dart
FutureResult<User?> signInWithGoogle() async {
  try {
    final res = await _supabaseService.signInWithGoogle();
    return Result.success(res);
  } catch (e) {
    return Result.failure();
  }
}
  • supabase_service.dart内のsignInWithGoogle() メソッドを呼び出し、Google認証を行います。
  • 認証が成功すればResult.success()でユーザー情報を返し、失敗した場合はResult.failure()でエラーハンドリングをしています。

FutureResult<User?>という型を使って、非同期処理の結果をよりわかりやすく管理できます。これにより、UI側でもスムーズに認証結果を処理できます。

Apple認証の実装

auth_repository.dart
FutureResult<User?> signInWithApple() async {
  try {
    final res = await _supabaseService.signInWithApple();
    return Result.success(res);
  } catch (e) {
    return Result.failure();
  }
}
  • こちらも同様にSupabaseServiceのsignInWithApple() を呼び出し、Apple認証を実行します。
  • 認証に成功すればユーザー情報を返し、失敗すればResult.failure()で適切に処理します。

メール認証の実装

auth_repository.dart
FutureResult<User?> signUpWithEmail(String email, String password) async {
  try {
    final res = await _supabaseService.signUpWithEmail(email, password);
    return Result.success(res);
  } catch (e) {
    return Result.failure();
  }
}
  • ユーザーのメールアドレスとパスワードを使った認証は、SupabaseServiceのsignUpWithEmail() で処理しています。
  • メール認証はシンプルな認証方法として多くのアプリで使われているため、このようにリポジトリ層で抽象化しておくと、後々変更があっても対応が簡単です。

サインアウト機能

auth_repository.dart
FutureResult<void> signOut() async {
  try {
    await _supabaseService.signOut();
    return Result.success(null);
  } catch (e) {
    return Result.failure();
  }
}
  • ユーザーがサインアウトする際には、SupabaseのsignOut() メソッドを呼び出します。
  • ここでもResult.success()とResult.failure()で成功と失敗の結果を明確に区別しています。

現在のユーザー情報を取得

auth_repository.dart
FutureResult<User?> getCurrentUser() async {
  try {
    final res = await _supabaseService.getCurrentUser();
    return Result.success(res);
  } catch (e) {
    return Result.failure();
  }
}
  • 現在サインインしているユーザーの情報を取得するためのメソッドです。
  • このメソッドをUI側で利用することで、ユーザーの状態を常に把握できるようにしています。

6.AuthStateの実装

Flutterアプリにおける認証の状態管理を、Riverpodを使って実装する方法を解説します。Supabaseをバックエンドに使い、Google、Apple、メール認証をサポートするアプリで、状態管理を行う重要なコンポーネントがAuthStateです。

AuthStateコード全体

以下がauth_state.dartの全体のコードになります。

auth_state.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../repositories/auth_repository.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

class AuthState extends StateNotifier<User?> {
  final AuthRepository _authRepository;

  AuthState(this._authRepository) : super(null);

  Future<void> signInWithGoogle() async {
    final result = await _authRepository.signInWithGoogle();
    result.when(success: (user) {
      state = user;
    }, failure: () {
      state = null;
    });
  }

  Future<void> signInWithApple() async {
    final result = await _authRepository.signInWithApple();
    result.when(success: (user) {
      state = user;
    }, failure: () {
      state = null;
    });
  }

  Future<void> signUpWithEmail(String email, String password) async {
    final result = await _authRepository.signUpWithEmail(email, password);
    result.when(success: (user) {
      state = user;
    }, failure: () {
      state = null;
    });
  }

  Future<void> signOut() async {
    final result = await _authRepository.signOut();
    result.when(success: (_) {
      state = null;
    }, failure: () {
      state = null;
    });
  }

  Future<void> getCurrentUser() async {
    final result = await _authRepository.getCurrentUser();
    result.when(success: (user) {
      state = user;
    }, failure: () {
      state = null;
    });
  }
}

// プロバイダーの定義
final authStateProvider = StateNotifierProvider<AuthState, User?>((ref) {
  return AuthState(AuthRepository());
});

AuthStateクラスの仕組み

AuthStateクラスは、StateNotifier<User?>を拡張して、Flutterアプリの認証状態を管理します。User?は現在ログインしているユーザーを表し、ログインしていない場合はnullになります。

StateNotifierを使うことで、状態の変更をリスナーに通知し、UIに反映させることができます。

コンストラクタ

auth_state.dart
AuthState(this._authRepository) : super(null);
  • _authRepositoryは、認証に関するビジネスロジックが集約されたリポジトリで、Supabaseを通じた認証操作を実行します。
  • 初期状態では、ユーザーはログインしていないので、super(null)でstateをnullに設定しています。

各認証メソッドの実装

Googleサインアップ

auth_state.dart
Future<void> signInWithGoogle() async {
  final result = await _authRepository.signInWithGoogle();
  result.when(success: (user) {
    state = user;
  }, failure: () {
    state = null;
  });
}
  • signInWithGoogle() は、Googleアカウントでサインアップするためのメソッドです。
  • _authRepository.signInWithGoogle() で認証処理を実行し、その結果を受け取ります。
  • 成功した場合はstateにユーザー情報をセットし、失敗した場合はnullをセットします。

Appleサインアップ

auth_state.dart
Future<void> signInWithApple() async {
  final result = await _authRepository.signInWithApple();
  result.when(success: (user) {
    state = user;
  }, failure: () {
    state = null;
  });
}
  • こちらもGoogleサインアップと同様に、_authRepository.signInWithApple() でAppleアカウントを用いたサインアップを行います。

メールアドレスサインアップ

auth_state.dart
Future<void> signUpWithEmail(String email, String password) async {
  final result = await _authRepository.signUpWithEmail(email, password);
  result.when(success: (user) {
    state = user;
  }, failure: () {
    state = null;
  });
}
  • ユーザーのメールアドレスとパスワードでサインアップを行うメソッドです。
  • 同様に成功すればユーザー情報を更新し、失敗すればstateをnullにします。

サインアウト

auth_state.dart
Future<void> signOut() async {
  final result = await _authRepository.signOut();
  result.when(success: (_) {
    state = null;
  }, failure: () {
    state = null;
  });
}
  • ユーザーのサインアウトを処理します。
  • 成功時にはstateをnullにすることで、ユーザーがログアウト状態であることを反映します。

現在のユーザー取得

auth_state.dart
Future<void> getCurrentUser() async {
  final result = await _authRepository.getCurrentUser();
  result.when(success: (user) {
    state = user;
  }, failure: () {
    state = null;
  });
}
  • サインインしているユーザーがいれば、その情報を取得します。
  • アプリの初期化時や、リロード時にサインイン状態を確認したい場合に使います。

プロバイダーの定義

auth_state.dart
final authStateProvider = StateNotifierProvider<AuthState, User?>((ref) {
  return AuthState(AuthRepository());
});
  • authStateProviderは、AuthStateのインスタンスを生成するためのStateNotifierProviderです。
  • authStateProviderを通じて、アプリ内のどこからでも認証状態を監視・操作できるようにします。

7.サインアップ認証画面の実装

SignupScreeenコード全体

以下が今回のサインアップ画面の全体のコードです。

signup_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../views/user_screen.dart';
import '../../state/auth_state.dart';

class SignupScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authStateProvider);

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("サインアップ"),
            ElevatedButton(
              onPressed: () {
                ref.read(authStateProvider.notifier).signInWithGoogle();
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => UserScreen()));
              },
              child: Text('Googleでサインアップ'),
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(authStateProvider.notifier).signInWithApple();
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => UserScreen()));
              },
              child: Text('Appleでサインアップ'),
            ),
            ElevatedButton(
              onPressed: () {
                showDialog(
                  context: context,
                  builder: (context) => EmailSignUpDialog(ref: ref),
                );
              },
              child: Text('メールでサインアップ'),
            ),
          ],
        ),
      ),
    );
  }
}

class EmailSignUpDialog extends StatefulWidget {
  final WidgetRef ref;

  const EmailSignUpDialog({super.key, required this.ref});

  
  _EmailSignUpDialogState createState() => _EmailSignUpDialogState();
}

class _EmailSignUpDialogState extends State<EmailSignUpDialog> {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text("メールでサインアップ"),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: emailController,
            decoration: InputDecoration(labelText: 'メールアドレス'),
          ),
          TextField(
            controller: passwordController,
            decoration: InputDecoration(labelText: 'パスワード'),
            obscureText: true,
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () {
            widget.ref
                .read(authStateProvider.notifier)
                .signUpWithEmail(emailController.text, passwordController.text);
            Navigator.push(
                context, MaterialPageRoute(builder: (context) => UserScreen()));
          },
          child: Text('サインアップ'),
        ),
      ],
    );
  }
}

サインアップ画面の概要

この画面では、ユーザーがGoogle、Apple、またはメールアドレスを使用してサインアップできるようにしています。Riverpodを利用して、ユーザーの認証状態を管理し、各プロバイダによるサインアップ処理を提供します。

SignupScreenクラス

SignupScreenはアプリのメインサインアップ画面です。ConsumerWidgetを使って、authStateProvider(Riverpodの状態管理プロバイダー)を監視し、サインアップの進行状況に応じてUIが更新されます。

各サインアップ方法

以下の3つの認証方法を提供しています:

Googleでサインアップボタン

signup_screen.dart
ElevatedButton(
  onPressed: () {
    context.read(authStateProvider.notifier).signInWithGoogle();
    Navigator.push(context, MaterialPageRoute(builder: (context) => UserScreen()));
  },
  child: Text('Googleでサインアップ'),
);

Appleでサインアップボタン

signup_screen.dart
ElevatedButton(
  onPressed: () {
    context.read(authStateProvider.notifier).signInWithApple();
    Navigator.push(context, MaterialPageRoute(builder: (context) => UserScreen()));
  },
  child: Text('Appleでサインアップ'),
);

メールでサインアップボタン

メールでのサインアップには、ダイアログが表示され、ユーザーはメールアドレスとパスワードを入力する形式になっています。showDialogを使ってEmailSignUpDialogを表示します。
EmailSignUpDialogは、ユーザーがメールアドレスとパスワードを入力してサインアップするためのダイアログです。

signup_screen.dart
class EmailSignUpDialog extends StatefulWidget {
  
  _EmailSignUpDialogState createState() => _EmailSignUpDialogState();
}
  • TextEditingController を使って、ユーザーの入力(メールアドレスとパスワード)を管理します。
  • サインアップボタンが押されると、context.read(authStateProvider.notifier).signUpWithEmailを呼び出し、ユーザーが入力したメールアドレスとパスワードでサインアップ処理を行います。

8.ユーザー情報表示画面の実装

サインアップが成功した後、ユーザーの情報を表示させる画面を作成していきます。この画面では、サインイン・サインアップが完了したユーザーの詳細情報を確認できるようにします。以下が、このユーザー情報表示画面の実装の手順です。

UserScreenコード全体

user_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../state/auth_state.dart';

class UserScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authStateProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('ユーザー情報'),
        actions: [
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: () {
              ref.read(authStateProvider.notifier).signOut();
            },
          ),
        ],
      ),
      body: Center(
        child: authState != null
            ? Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('ようこそ、${authState.email}!'),
                  SizedBox(height: 8),
                  Text('ユーザーID: ${authState.id}'),
                  SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () {
                      ref.read(authStateProvider.notifier).signOut();
                    },
                    child: Text('サインアウト'),
                  ),
                ],
              )
            : Text('ログインしてください'),
      ),
    );
  }
}

UserScreen では、サインイン・サインアップが成功したユーザーの情報(メールアドレスとユーザーID)を表示します。Riverpodの authStateProvider を利用して、認証状態を監視し、サインイン済みのユーザー情報を反映する仕組みです。加えて、サインアウトボタンを設置して、いつでもログアウトできるようにしています。

Riverpodのプロバイダーを監視

user_screen.dart
final authState = watch(authStateProvider);
  • authState に現在のユーザー情報が格納され、UIに反映されます。ユーザーがサインインしている場合は、authState からメールアドレスとユーザーIDを取得します。

AppBarのサインアウトボタン

user_screen.dart
IconButton(
  icon: Icon(Icons.logout),
  onPressed: () {
    context.read(authStateProvider.notifier).signOut();
  },
),
  • 画面右上の AppBar にサインアウトボタンを配置しています。ボタンをタップすると、authStateProvider.notifier の signOut() メソッドが呼び出され、ユーザーがサインアウトされます。

サインイン状態に応じたUI表示

サインイン済みの場合:

user_screen.dart
authState != null
    ? Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('ようこそ、${authState.email}!'),
          SizedBox(height: 8),
          Text('ユーザーID: ${authState.id}'),
          SizedBox(height: 16),
          ElevatedButton(
            onPressed: () {
              context.read(authStateProvider.notifier).signOut();
            },
            child: Text('サインアウト'),
          ),
        ],
      )
    : Text('ログインしてください'),
  • ユーザーのメールアドレスとユーザーIDを表示し、サインアウトボタンを用意します。

サインインしていない場合:

user_screen.dart
: Text('ログインしてください'),
  • ユーザーがサインインしていない場合は、「ログインしてください」というメッセージが表示されます。

サインアウトボタンの処理

user_screen.dart
context.read(authStateProvider.notifier).signOut();
  • ユーザーがサインアウトボタンをタップすると、以下のメソッドが呼ばれ、サインアウト処理が行われます。
  • サインアウトが完了すると authState が null になるため、UIも自動的にログアウト状態を反映し、「ログインしてください」というメッセージに切り替わります。

9.まとめ

以上、FlutterとSupabaseを使って、Google、Apple、メール認証を実装し、さらにRiverpodで効率的に認証状態を管理する方法を見てきました。さらに、Resultを活用することでエラーハンドリングもシンプルに行えました。

アプリ全体の認証フローが整ったことで、サインイン・サインアップから、ユーザー情報の表示、そしてサインアウトまで、一連の流れをスムーズに実装できたと思います。

認証はほとんどのアプリで必須の機能なので、今回の実装をベースにして、自分のアプリにも取り入れてみてください。もちろん、アレンジして自分の要件に合わせたカスタマイズもどんどん試してみると良いでしょう!

それでは、楽しいFlutter開発を!

Discussion