[Flutter,Riverpod,Firebase]MVCを意識してサインアップ機能とログイン機能を実装してみよう
よろしくお願いします!
MVCとは
Image Credit: FlutterDevs
- Model(データ管理): アプリケーションのデータとビジネスロジックを担当。データの保存、管理、操作を行います。
- View(UI): ユーザーに情報を表示し、ユーザーの入力を受け取るインターフェースです。見た目(UI)に関わります。
- Controller(ロジック制御): モデルとビューを結びつけ、ユーザーの入力に基づいてモデルを更新し、その結果をビューに反映させます。
MVCのメリット
- 分離と専門化: MVCは責任の明確な分離を提供します。これにより、デザイナーはビューに、開発者はモデルとコントローラーに集中できます。
- 再利用性と拡張性: 各コンポーネントは独立しているため、アプリケーションの一部を変更しても他の部分に影響を与えにくいです。これにより、アプリケーションの再利用性と拡張性が向上します。
- 保守性: コードの構造化が促進されるため、アプリケーションの保守が容易になります。
今回のデモアプリ
今回はfirebase
を使ってメールアドレスとパスワードで、サインアップ機能(新規ユーザー登録) と ログイン機能(既存のユーザーを認証) を実装していきたいと思います。
pubspec.yaml
cupertino_icons: ^1.0.6
firebase_core:
firebase_auth:
cloud_firestore:
firebase_database:
firebase_storage:
flutter_svg: ^2.0.10+1 //画像の表示ため
flutter_riverpod: ^2.4.10
freezed: ^2.4.7
freezed_annotation: ^2.4.1
image_picker: ^1.0.7
carousel_slider: ^4.2.1
dev_dependencies:
build_runner: ^2.4.8
flutter_test:
sdk: flutter
json_serializable: ^6.7.1
-
sinup_viewでユーザーをauthticationとfirestoreに登録
-
login_viewでユーザーを認証する
-
home_viewに画面遷移したら成功!
UIの流れ
sinup_viewでユーザーをauthとfirestoreに登録
👇
👇
login_viewでユーザーを認証
👇
👇
home_viewに画面遷移したら成功!⭕️
フォルダ構成
lib/
core/
providers.dart
features/
auth/
controller/
auth_controller.dart
view/
login_view.dart
signup_view.dart
widgets/
auth_field.dart
home/
view/
home_view.dart
firebase/
auth_api.dart
user_api.dart
models/
user_model.dart
user_model.freezed.dart
user_model.g.dart
main.dart
core/
-
providers.dart
:アプリ全体で使用するRiverpodプロバイダーなどのコアとなる依存性を管理します。コア機能やサービスの設定を含みます。
features/
-
auth/
:認証機能に関連するファイルを格納します。-
controller/auth_controller.dart
:認証プロセスのビジネスロジックを担当します。モデルとビュー間の仲介役として機能します。 -
view/
:UIコンポーネントであるログインビューとサインアップビューを含みます。ユーザーに情報を表示し、ユーザー入力を受け取ります。 -
widgets/auth_field.dart
:認証ビューで再利用されるカスタムウィジェット(例えば、カスタマイズされたテキストフィールド)を含みます。
-
-
home/
:ホーム画面に関連するファイルを格納します。-
view/home_view.dart
:アプリのホーム画面のUIを定義します。
-
firebase/
-
firebase/
:アプリケーションのFirebase関連の機能を管理します。このフォルダは、Firebase AuthenticationとFirestoreデータベースとの直接的なインタラクションを担うクラスを含み、アプリケーション内で必要とされる認証とデータ管理の機能を提供します。 -
auth_api.dart
: Firebase Authenticationサービスとのインタラクションを担います。新規ユーザーのサインアップ、既存ユーザーのログイン、および現在のユーザー情報の取得など、認証に関連するビジネスロジックを管理します。 -
user_api.dart
: Firestoreデータベースとのインタラクションを担います。ユーザーデータの保存と取得に関わる操作を管理し、アプリケーションでのユーザー情報の永続化を支援します。
models/
-
user_model.dart
、user_model.freezed.dart
、user_model.g.dart
:アプリケーションのモデル層で、データ構造とビジネスロジック(特にデータの扱いや変換)を定義します。freezedとg.dartは、コード生成による部分で、Dartのデータモデリングや状態管理を効率化します。
Model(models/)
MVCの[M]
Modelはアプリケーションのデータ管理とビジネスロジックを担うMVCアーキテクチャの核心部分です。Model層はUI(View)やユーザー操作(Controller)から独立しており、データの永続化、更新、取得を司ります。また、Firebaseとの通信を通じて、データのリモート管理を行います。
user_model.dartの概要
UserModelの役割と構造
UserModelは、Firestoreに保存されるデータの構造を定義しています。
コード解説
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_model.freezed.dart';
part 'user_model.g.dart';
class UserModel with _$UserModel {
const factory UserModel({
required String email,
required String name,
(includeToJson: false) ("") String uid, // toJsonの際に除外
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
このコードでは、freezedパッケージを使用し、@freezed
アノテーションで、コードを自動生成しています。
@JsonKey(includeToJson: false)
アノテーションを使ってuidフィールドをJSONシリアライズの対象から除外しています。
これは、Firebaseでは、一意のユーザーID(uid)がデータベースのドキュメントIDとして使われるため、データ本体に含める必要がなく、セキュリティ上も外部に公開すべきではないからです。
ポイントのまとめ
-
データモデルの役割:
UserModel
はアプリケーションでユーザー情報を管理するための中心的なデータ構造です。 - uidの取り扱い: Firebaseでのユーザー識別子(uid)はデータモデルには含まれますが、セキュリティとデータベース設計の観点から、JSONへのシリアライズ時には除外されます。
-
コード自動生成の活用:
freezedパッケージ
を利用することで、データモデルの定義を簡潔に保ちつつ、開発効率とコードの品質を向上させます。
auth_api.dartの概要
Firebase Authenticationを利用してユーザー認証機能(サインアップ、ログイン、現在のユーザー情報の取得)を提供する役割を持っています。
コード解説
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/failure.dart';
import '../core/providers.dart';
// FirebaseAuthAPIインスタンスをアプリケーションの他の部分で利用できるようにする。
final authAPIProvider = Provider((ref) {
final auth = ref.watch(firebaseAuthProvider);
return FirebaseAuthAPI(auth: auth);
});
abstract class AuthAPI {
Future<User?> signUp({required String email, required String password});
Future<User?> login({required String email, required String password});
User? currentUser();
}
class FirebaseAuthAPI implements AuthAPI {
final FirebaseAuth _auth;
FirebaseAuthAPI({required FirebaseAuth auth}) : _auth = auth;
// メールアドレスとパスワードを使用して新規ユーザーをFirebaseに登録します。
Future<User?> signUp({required String email, required String password}) async {
try {
final userCredential = await _auth.createUserWithEmailAndPassword(email: email, password: password);
return userCredential.user;
} on FirebaseAuthException catch (e) {
throw Failure(message: e.message ?? "サインアップ中に予期せぬエラーが発生しました。");
}
}
// メールアドレスとパスワードを使用して既存のユーザーを認証します。
Future<User?> login({required String email, required String password}) async {
try {
final userCredential = await _auth.signInWithEmailAndPassword(email: email, password: password);
return userCredential.user;
} on FirebaseAuthException catch (e) {
throw Failure(message: e.message ?? "ログイン中に予期せぬエラーが発生しました。");
}
}
// 現在のユーザー (currentUserメソッド): 現在ログインしているFirebaseユーザーの情報を返します。
User? currentUser() {
return _auth.currentUser;
}
}
FirebaseAuthAPIクラスがAuthAPIインターフェースを実装している点に注目します。
ここで、Firebase Authenticationの主要なメソッド(signUp, login, currentUser)を定義しています。
ポイントのまとめ
- 抽象化と実装: AuthAPIインターフェースによって認証メソッドの抽象化を行い、FirebaseAuthAPIクラスでこれらのメソッドを具体的に実装しています。これにより、アプリケーションの他の部分は、具体的な認証サービスの実装詳細を意識せずに認証機能を利用できるようになります。
-
アプリケーション全体へのアクセス性:
authAPIProvider
を通じて、アプリケーションのどこからでも認証機能にアクセスできるようにします。これにより、認証状態の管理が一元化され、状態管理の複雑性が軽減されます。
user_api.dartの概要
Firebase Firestoreを用いてユーザーデータを保存し、取得するための機能を提供する役割を持っています。
このクラスはアプリ内のユーザー情報の保存とアクセスを管理し、Firebase Firestoreとの間でデータを同期します。ここでは、saveUserData
メソッドでユーザー情報をFirestoreに保存し、getUserData
メソッドで特定のUID
を持つユーザーの情報を取得する機能を提供します。
コード解説
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:x_clone_firebase/core/providers.dart';
import '../core/failure.dart';
import '../models/user_model.dart';
// FirebaseUserAPIインスタンスをアプリケーションの他の部分で利用できるようにする。
final userAPIProvider = Provider((ref) {
return FirebaseUserAPI(firestore: ref.watch(firestoreProvider));
});
abstract class UserAPI {
Future<void> saveUserData(UserModel userModel);
Future<Map<String, dynamic>?> getUserData(String uid);
}
class FirebaseUserAPI implements UserAPI {
final FirebaseFirestore _firestore;
FirebaseUserAPI({required FirebaseFirestore firestore}) : _firestore = firestore;
// Firestoreの`users`コレクションにユーザー情報を保存します。ユーザーモデルをJSONに変換して使用します。
Future<void> saveUserData(UserModel userModel) async {
try {
await _firestore.collection("users").doc(userModel.uid).set(userModel.toJson());
} on FirebaseException catch (e) {
throw Failure(message: e.message ?? "Some unexpected error occurred");
} catch (e) {
throw Failure(message: e.toString());
}
}
// firestoreにユーザーデータが存在する場合、そのデータをMap<String, dynamic>として返します。
Future<Map<String, dynamic>?> getUserData(String uid) async {
try {
DocumentSnapshot userSnapshot = await _firestore.collection("users").doc(uid).get();
if (userSnapshot.exists) {
// ユーザーデータが存在する場合、そのデータをMap<String, dynamic>として返します。
return userSnapshot.data() as Map<String, dynamic>?;
} else {
// ユーザーデータが存在しない場合はnullを返します。
return null;
}
} on FirebaseException catch (e) {
// Firestoreからデータを取得中に発生したエラーを処理します。
throw Failure(message: e.message ?? "An unexpected error occurred");
} catch (e) {
// その他のエラーを処理します。
throw Failure(message: e.toString());
}
}
}
user_api.dart
ファイルは、Firebase Firestoreとのインタラクションを担当するFirebaseUserAPIクラスを定義しています。このクラスはUserAPIインターフェイスを実装し、Firestoreのusersコレクション
にユーザーデータを保存または取得する具体的な方法を提供します。
-
FirebaseUserAPIインスタンスの提供:
userAPIProvider
を用いて、アプリ全体からFirebaseUserAPIインスタンスにアクセスできるようにします。
これはRiverpodを使用して実装され、FirestoreインスタンスはfirestoreProvider
から取得されます。 -
saveUserDataメソッド
(データの保存):UserModel
のインスタンスを受け取り、そのデータをFirestoreのusersコレクション
に保存します。データはuserModel.toJson()を呼び出すことでJSON形式に変換され、ドキュメントIDとしてユーザーのUIDが使用されます。 -
getUserDataメソッド
(データの取得):指定されたUIDを持つユーザーのデータをFirestoreから非同期に取得します。データが存在する場合はUserModel.fromJson
を用いて取得したデータをUserModel
に変換し、返します。
ポイントのまとめ
-
データ永続化とアクセス:
FirebaseUserAPIクラス
はFirebase Firestoreを介してアプリケーションのユーザーデータを管理します。これにより、アプリのどこからでもユーザーデータの保存と取得が可能になります。 -
Riverpodの利用:
userAPIProvider
を通じて、FirebaseUserAPI
インスタンスへのアクセスをアプリ全体で容易にします。これにより、状態管理が一元化され、コードの可読性と保守性が向上します。 -
セキュリティとデータ整合性: ユーザー情報の取り扱いには、セキュリティが重要です。Firestoreの
usersコレクション
へのアクセス制御を適切に設定し、データの整合性を保つことが重要です。
View (features/auth/view/)
MVCの[V]
View(UI): ユーザーに情報を表示し、ユーザーの入力を受け取るインターフェースです。見た目(UI)に関わります。
SignUpView
signup_view
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../common/common.dart';
import '../../../constants/ui_constants.dart';
import '../../../theme/theme.dart';
import '../controller/auth_controller.dart';
import '../widgets/auth_field.dart';
import 'login_view.dart';
class SignUpView extends ConsumerStatefulWidget {
static route() => MaterialPageRoute(
builder: (context) => SignUpView(),
);
const SignUpView({super.key});
ConsumerState<SignUpView> createState() => _SignUpViewState();
}
class _SignUpViewState extends ConsumerState<SignUpView> {
final appbar = UIConstants.appBar();
final emailController = TextEditingController();
final passwordController = TextEditingController();
void dispose() {
super.dispose();
emailController.dispose();
passwordController.dispose();
}
void onSignUp() {
ref.read(authControllerProvider.notifier).signUp(
email: emailController.text,
password: passwordController.text,
context: context,
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: appbar,
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
// e-mail
AuthField(
controller: emailController,
hintText: 'Email address',
),
SizedBox(height: 25),
// password
AuthField(
controller: passwordController,
hintText: 'パスワード',
),
SizedBox(height: 40),
// button
Align(
alignment: Alignment.topRight,
child: RoundedSmallButton(
onTap: onSignUp, // sign up 処理
label: "Done",
),
),
SizedBox(height: 40),
RichText(
text: TextSpan(
text: "Don't have an account?",
style: TextStyle(
fontSize: 16,
),
children: [
TextSpan(
text: " Login",
style: TextStyle(
color: Pallete.blueColor,
fontSize: 16,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.push(
context,
LoginView.route(),
);
},
),
]),
),
],
),
),
),
),
);
}
}
主要なコンポーネント
- E-mailフィールド
- パスワードフィールド
- 完了ボタン("Done"): ユーザーが入力した情報でサインアップを試みるためのボタンです。タップすると、入力されたメールアドレスとパスワードで新規アカウントの作成が開始されます。
- ログインリンク: すでにアカウントを持っているユーザーがログイン画面へ移動できるリンクです。"Don't have an account? Login"というテキストで、ユーザーがタップするとログイン画面(LoginView)へ遷移します。
機能
- 新規登録: ユーザーが提供したメールアドレスとパスワードを使用してFirebase Authを介して新規登録を行います。登録成功後、ユーザーは
login_view
に画面遷移されます。 - 入力検証: メールアドレスとパスワードの入力が適切かどうかを検証し、不適切な場合はユーザーにフィードバックを提供します。
- 画面遷移: ユーザーがすでにアカウントを持っている場合、ログイン画面へ簡単に移動できるようにします。
LoginView
login_view
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:x_clone_firebase/features/auth/view/signup_view.dart';
import '../../../common/common.dart';
import '../../../constants/ui_constants.dart';
import '../../../theme/pallete.dart';
import '../controller/auth_controller.dart';
import '../widgets/auth_field.dart';
class LoginView extends ConsumerStatefulWidget {
static route() => MaterialPageRoute(
builder: (context) => LoginView(),
);
const LoginView({super.key});
ConsumerState<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends ConsumerState<LoginView> {
final appbar = UIConstants.appBar();
final emailController = TextEditingController();
final passwordController = TextEditingController();
void dispose() {
super.dispose();
emailController.dispose();
passwordController.dispose();
}
void onLogin() {
ref.read(authControllerProvider.notifier).login(
email: emailController.text,
password: passwordController.text,
context: context,
);
}
Widget build(BuildContext context) {
final isLoading = ref.watch(authControllerProvider);
return Scaffold(
appBar: appbar,
body: isLoading
? const Loader()
: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
// e-mail
AuthField(
controller: emailController,
hintText: 'Email address',
),
SizedBox(height: 25),
// password
AuthField(
controller: passwordController,
hintText: 'パスワード',
),
SizedBox(height: 40),
// button
Align(
alignment: Alignment.topRight,
child: RoundedSmallButton(
onTap: onLogin, // ログイン処理
label: "Done",
),
),
SizedBox(height: 40),
RichText(
text: TextSpan(
text: "Don't have an account?",
style: TextStyle(
fontSize: 16,
),
children: [
TextSpan(
text: " Sign up",
style: TextStyle(
color: Pallete.blueColor,
fontSize: 16,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Navigator.push(
context,
SignUpView.route(),
);
},
),
]),
),
],
),
),
),
),
);
}
}
主要なコンポーネント
- E-mailフィールド
- パスワードフィールド
- 完了ボタン("Done"): 入力された情報を用いてログインを実行するボタンです。タップすると、認証プロセスが開始されます。
- サインアップリンク: アカウントを持っていないユーザーがサインアップ画面に遷移できるリンクです。"Don't have an account? Sign up"と表示され、新規ユーザーを歓迎します。
機能
- 認証: ユーザーはメールアドレスとパスワードを使用して認証を試みます。認証が成功すれば、
home_view
に画面遷移します。 - 入力検証: 入力されたメールアドレスとパスワードが適切であるか検証され、問題がある場合はユーザーに通知されます。
- アカウント登録: 新規ユーザーは、サインアップリンクをタップすることで登録画面に遷移できます。
widgets/auth_field.dart
TextFieldはwidgetsに書いて共通化しています。
auth_field.dart
import 'package:flutter/material.dart';
import '../../../theme/theme.dart';
class AuthField extends StatelessWidget {
final TextEditingController controller;
final String hintText;
const AuthField(
{super.key, required this.controller, required this.hintText});
Widget build(BuildContext context) {
return TextField(
style: TextStyle(
fontWeight: FontWeight.bold,
),
controller: controller,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Pallete.blueColor, width: 3),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(
color: Pallete.greyColor,
),
),
contentPadding: EdgeInsets.all(22),
hintText: hintText,
hintStyle: TextStyle(
fontSize: 18,
)),
);
}
}
Controller(features/auth/controller/)
MVCの[C]
ControllerはアプリケーションのUI(View)とデータ(Model)の間の仲介役です。ユーザーからの入力を受け、それに基づいてModelを更新し、変更をViewに反映させます。このプロセスにより、ユーザーインタラクションとアプリケーションのロジックが密接に結びつきます。
auth_controller.dartの概要
役割と機能
AuthController
はViewから受け取ったユーザーのメールアドレスとパスワードをModel層へと渡し、Firebase Authentication
を介してサインアップやログインを実行します。認証成功後のユーザーデータは、同じくModel層のFirebase Firestore
へ保存されます。
コード解説
auth_controller.dartの全体コード
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/failure.dart';
import '../../../core/utils.dart';
import '../../../firebase/auth_api.dart';
import '../../../firebase/user_api.dart';
import '../../../models/user_model.dart';
import '../../home/view/home_view.dart';
import '../view/login_view.dart';
// アプリケーション全体でAuthControllerを利用可能にします。これにより、認証状態の管理とFirebase認証機能へのアクセスが行えます。
final authControllerProvider = StateNotifierProvider<AuthController, bool>((ref) {
return AuthController(
authApi: ref.watch(authAPIProvider),
userApi: ref.watch(userAPIProvider),
);
});
// 現在ログインしているユーザーの詳細情報を非同期で取得します。
final currentUserDetailsProvider = FutureProvider((ref) async {
final currentUserId = ref.watch(currentUserAccountProvider).value!.uid;
final userDetails = await ref.watch(userDetailsProvider(currentUserId).future);
return userDetails;
});
// 特定のUIDを持つユーザーの詳細情報を非同期で取得します。
final userDetailsProvider = FutureProvider.family<UserModel, String>((ref, String uid) async {
final authController = ref.watch(authControllerProvider.notifier);
return await authController.getUserData(uid);
});
// AuthControllerを通じてFirebaseAuthから現在ログインしているユーザーの情報を取得します。
final currentUserAccountProvider = FutureProvider<User?>((ref) async {
final authController = ref.watch(authControllerProvider.notifier);
return authController.currentUser();
});
class AuthController extends StateNotifier<bool> {
// FirebaseAuthAPIを介してFirebase Authenticationの機能にアクセスします。
final FirebaseAuthAPI _authAPI;
// FirebaseUserAPIを介してFirestoreに保存されたユーザーデータの操作を行います。
final FirebaseUserAPI _userAPI;
AuthController({required FirebaseAuthAPI authApi, required FirebaseUserAPI userApi})
: _authAPI = authApi,
_userAPI = userApi,
super(false);
// 現在ログインしているユーザーをFirebaseAuthから取得します。
User? currentUser() => _authAPI.currentUser();
// 新規ユーザーをFirebaseに登録し、Firestoreにユーザー情報を保存します。
void signUp({
required String email,
required String password,
required BuildContext context,
}) async {
state = true;
try {
final user = await _authAPI.signUp(email: email, password: password);
UserModel userModel = UserModel(
email: email,
name: getNameFromEmail(email),
followers: const [],
following: const [],
profilePic: '',
bannerPic: '',
uid: user!.uid,
bio: '',
isTwitterBlue: false,
);
await _userAPI.saveUserData(userModel);
showSnackBar(context, 'Account created! Please login.');
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoginView()));
} on Failure catch (f) {
showSnackBar(context, f.message);
} finally {
state = false;
}
}
// メールアドレスとパスワードを使用してログインを試み、成功した場合はホーム画面に遷移します。
void login({
required String email,
required String password,
required BuildContext context,
}) async {
state = true;
try {
await _authAPI.login(email: email, password: password);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => HomeView()));
} on Failure catch (f) {
showSnackBar(context, f.message);
} finally {
state = false;
}
}
// 指定されたUIDに基づきFirestoreからユーザーデータを取得し、UserModelに変換して返します。
Future<UserModel> getUserData(String uid) async {
try {
final document = await _userAPI.getUserData(uid);
return UserModel.fromJson(document as Map<String, dynamic>);
} on Failure catch (f) {
throw Exception(f.message);
}
}
}
1.authControllerProvider
final authControllerProvider = StateNotifierProvider<AuthController, bool>((ref) {
return AuthController(
authApi: ref.watch(authAPIProvider),
userApi: ref.watch(userAPIProvider),
);
});
-
StateNotifierProviderを用いた
authController
の提供:
authControllerProvider
はStateNotifierProvider
を用いて定義されており、アプリケーション全体でAuthController
のインスタンスにアクセスできるようにします。これにより、認証状態の変化をアプリケーション全体でリアクティブに扱うことが可能になります。
2.ユーザー情報の非同期取得
// 現在ログインしているユーザーの詳細情報を非同期で取得します。
final currentUserDetailsProvider = FutureProvider((ref) async {
final currentUserId = ref.watch(currentUserAccountProvider).value!.uid;
final userDetails = await ref.watch(userDetailsProvider(currentUserId).future);
return userDetails;
});
// 特定のUIDを持つユーザーの詳細情報を非同期で取得します。
final userDetailsProvider = FutureProvider.family<UserModel, String>((ref, String uid) async {
final authController = ref.watch(authControllerProvider.notifier);
return await authController.getUserData(uid);
});
// AuthControllerを通じてFirebaseAuthから現在ログインしているユーザーの情報を取得します。
final currentUserAccountProvider = FutureProvider<User?>((ref) async {
final authController = ref.watch(authControllerProvider.notifier);
return authController.currentUser();
});
-
ユーザー情報プロバイダー:
-
currentUserDetailsProviderは、現在ログインしているユーザーの詳細情報を非同期で取得します。これは
FutureProvider
を使用しており、アプリケーション内のどこからでも現在のユーザー情報へのアクセスを可能にします。 -
userDetailsProviderは、特定のユーザーIDに基づいてそのユーザーの詳細情報を取得するために使用されます。これも
FutureProvider.family
を使い、動的な引数(この場合はユーザーID)に基づいてデータを提供します。これにより、特定のユーザーに関する情報を柔軟に取得することが可能になります。 -
currentUserAccountProviderは、
AuthController
を通じてFirebase Authentication
から現在ログインしているユーザーのアカウント情報を取得します。これはアプリケーションのさまざまな場所でユーザーがログインしているかどうかを判断するために利用されます。
-
3.AuthControllerクラス
class AuthController extends StateNotifier<bool> {
// FirebaseAuthAPIを介してFirebase Authenticationの機能にアクセスします。
final FirebaseAuthAPI _authAPI;
// FirebaseUserAPIを介してFirestoreに保存されたユーザーデータの操作を行います。
final FirebaseUserAPI _userAPI;
AuthController({required FirebaseAuthAPI authApi, required FirebaseUserAPI userApi})
: _authAPI = authApi,
_userAPI = userApi,
super(false);
// 現在ログインしているユーザーをFirebaseAuthから取得します。
User? currentUser() => _authAPI.currentUser();
// 新規ユーザーをFirebaseに登録し、Firestoreにユーザー情報を保存します。
void signUp({
required String email,
required String password,
required BuildContext context,
}) async {
state = true;
try {
final user = await _authAPI.signUp(email: email, password: password);
UserModel userModel = UserModel(
email: email,
name: getNameFromEmail(email),
followers: const [],
following: const [],
profilePic: '',
bannerPic: '',
uid: user!.uid,
bio: '',
isTwitterBlue: false,
);
await _userAPI.saveUserData(userModel);
showSnackBar(context, 'Account created! Please login.');
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoginView()));
} on Failure catch (f) {
showSnackBar(context, f.message);
} finally {
state = false;
}
}
// メールアドレスとパスワードを使用してログインを試み、成功した場合はホーム画面に遷移します。
void login({
required String email,
required String password,
required BuildContext context,
}) async {
state = true;
try {
await _authAPI.login(email: email, password: password);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => HomeView()));
} on Failure catch (f) {
showSnackBar(context, f.message);
} finally {
state = false;
}
}
// 指定されたUIDに基づきFirestoreからユーザーデータを取得し、UserModelに変換して返します。
Future<UserModel> getUserData(String uid) async {
try {
final document = await _userAPI.getUserData(uid);
return UserModel.fromJson(document as Map<String, dynamic>);
} on Failure catch (f) {
throw Exception(f.message);
}
}
}
-
サインアップとログイン機能:
signUpメソッド
とloginメソッド
は、Firebase Authentication
を使ってユーザーの認証を行います。サインアップでは新規ユーザーをFirebase
に登録し、成功すればFirestore
にユーザーデータを保存します。ログインでは既存のユーザー認証を試み、成功すればホーム画面に遷移します。 -
ユーザーデータの取得と管理:
getUserDataメソッド
は、特定のユーザーIDに基づいてFirestore
からユーザーデータを取得し、それをUserModel
に変換して返します。これにより、アプリケーション内で必要なユーザー情報を簡単に取得できます。
Core(MVCとは直接は関係ない)
それぞれのファイルで定義したRiverpodを、アプリケーション全体で簡単にアクセスできるようにProviderを設定しています。
Core/providers
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Firebase Authenticationのインスタンスを提供するProvider
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
return FirebaseAuth.instance;
});
// 現在ログインしているユーザーを追跡するProvider
final authStateChangesProvider = StreamProvider<User?>((ref) {
return ref.watch(firebaseAuthProvider).authStateChanges();
});
// Cloud Firestoreのインスタンスを提供するProvider
final firestoreProvider = Provider<FirebaseFirestore>((ref) {
return FirebaseFirestore.instance;
});
// Firebase Storageのインスタンスを提供するProvider
final storageProvider = Provider<FirebaseStorage>((ref) {
return FirebaseStorage.instance;
});
main.dart
currentUserAccountProvider
を使って、現在ログインしているユーザーの情報を監視します。このプロバイダーは、ユーザーがログインしているかどうかに基づいて異なる状態(データ、エラー、ローディング)を返します。
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'common/error_page.dart';
import 'common/loading_page.dart';
import 'features/auth/controller/auth_controller.dart';
import 'features/auth/view/signup_view.dart';
import 'features/home/view/home_view.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context,WidgetRef ref) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: ref.watch(currentUserAccountProvider).when(
data: (user) {
if (user != null) {
// ユーザーがログインしている場合
return const HomeView();
}
// ユーザーがログインしていない場合
return SignUpView();
},
// エラーになった時
error: (e, st) => ErrorPage(
error: e.toString(),
),
// ロード中
loading: () => LoadingPage(),
)
);
}
}
- 条件に応じた画面表示:
- ログイン済み: ユーザーがログインしている場合、ホーム画面 (HomeView) を表示します。
- 未ログイン: ユーザーがログインしていない場合、サインアップ画面 (SignUpView) を表示します。
- ローディング中: ユーザー情報の取得中は、ローディング画面 (LoadingPage) を表示します。
- エラー発生: ユーザー情報の取得時にエラーが発生した場合、エラー画面 (ErrorPage) を表示します。
実際の処理の流れ
signUp機能の流れ
- view: メールアドレス、パスワードを入力する、
onSignUp()
👇 - contorller:
signUp()
👇 - model:
FirebaseAuthAPI
のFuture<User?> signUp
👇 - model:
FirebaseUserAPI
のFuture<void> saveUserData(UserModel userModel)
👇
画面遷移⭕️
login機能の流れ
- view: メールアドレス、パスワードを入力する、
onLogin()
👇 - contorller:
login()
👇 - model:
FirebaseAuthAPI
のFuture<User?> login
👇
画面遷移⭕️
自動ログインの流れ
- main.dart:
currentUserAccountProvider
が呼ばれる
👇 - contorller:
User? currentUser() => _authAPI.currentUser();
👇 - main.dart: Userが返ってきたら、
return const HomeView();
画面遷移⭕️
まとめ
今回は、MVCアーキテクチャを用いてFirebaseを活用し、サインアップ機能(新規ユーザー登録)とログイン機能(既存ユーザーの認証)を実装してみました。私はこれまで特にアーキテクチャにこだわらずにコーディングをしてきました。特に個人開発では、その必要性を感じることは少なかったですが、Flutterエンジニアとしてのキャリアを目指し、就職活動で自身の作品をポートフォリオとして提出する際、企業からアーキテクチャに関する質問を受けたり、コードを詳細に見てもらう機会が多くありました。その経験から、コードはなるべくクリーンに保つ重要性を実感しました。
長文になりましたが、最後までお読みいただき、ありがとうございました!
今回使った主なパッケージ
freezed
freezed_annotation
riverpod
state_provider
future_provider
参考にした動画
Discussion