初めてFlutterを触ってみた- Flutter × Laravel 認証- ①
初めてFlutterを触ってみた
Flutterを初めて触りながら、LaravelのSanctumを使って認証機能を実装してみました。調べていると、Firebaseの認証を使う例が多いようですが、今回はLaravelでAPIを用意して、認証しています。
この記事では、Laravelの詳細には触れず、Flutterの実装に焦点を当て、認証機能の内容を共有します。まだFlutterに慣れていないので、皆さんのアドバイスやフィードバックもぜひお待ちしています!
実装した機能
以下の機能を実装しました。
- アカウント登録/退会
- 認証機能
- 二段階認証
- データ取得/表示
- アカウント情報の変更
使用したパッケージ
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
provider: ^6.1.2
shared_preferences: ^2.3.2
flutter_dotenv: ^5.1.0
shimmer: ^3.0.0
http: ^1.2.2
flutter_html: ^3.0.0-beta.2
Laravel側では、認証にSanctumを使用しました。Flutterではshared_preferences
とprovider
を組み合わせて認証情報を管理しています。
ディレクトリ構成
最終的なディレクトリ構成は以下のようになりました。
lib/
├── main.dart # アプリのエントリーポイント
├── src
│ ├── models/ # データモデルクラス
│ ├── screens/ # 各画面のUIを定義するファイル
│ ├── widgets/ # 再利用可能なウィジェット
│ ├── services/ # APIやデータベースとのやり取りを行うサービス
│ ├── providers/ # 状態管理用のプロバイダー
│ ├── utils/ # ユーティリティ関数やヘルパー
│ ├── themes # アプリのテーマ
│ ├── constants/ # 定数や共通の値
└── ・・・
認証のフロー図
以下のフロー図は、アプリの起動から認証、各画面への遷移の流れを抜粋して表したものです。
全体はコチラです
よく利用するユーティリティ
ここでは、今後何度も使用するユーティリティクラスを紹介します。
DialogUtils
API接続時などに、ローディングの表示/非表示を行います。
import 'package:flutter/material.dart';
class DialogUtils {
// ローディングダイアログを表示
static void showLoadingDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false, // タップで閉じられないようにする
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(), // ローディングスピナーを表示
);
},
);
}
// ローディングダイアログを非表示にする
static void hideLoadingDialog(BuildContext context) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop(); // ダイアログを閉じる
}
}
}
RouteUtils
認証情報の変更後、AuthWrapper
へ遷移する際に使用します。
import 'package:flutter/material.dart';
import 'package:flutter_sample/src/widgets/auth_wrapper.dart';
class RouteUtils {
static void navigateToAuthWrapper(BuildContext context) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const AuthWrapper()),
(route) => false, // 以前の画面を削除する
);
}
}
SnackbarUtils
一時的なメッセージ(例:エラーメッセージや成功通知)を表示するために使用します。
import 'package:flutter/material.dart';
class SnackbarUtils {
// 共通のSnackbarを表示するメソッド
static void showSnackbar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
message,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.black, // 背景色も設定できます
duration: const Duration(seconds: 1), // 表示時間もカスタマイズ可能
),
);
}
}
アプリ起動からAuthWrapper遷移まで
以下のコードでは、アプリのエントリーポイントとスプラッシュ画面を定義しています。スプラッシュ画面では、アプリ起動時に認証情報をチェックし、認証状態に応じてAuthWrapper
へ遷移させます。
lib/main.dart
アプリ全体のテーマやルート設定、initialRouteをSplashScreenに指定しています。
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_sample/src/providers/auth_provider.dart';
import 'package:flutter_sample/src/screens/auth/login_screen.dart';
import 'package:flutter_sample/src/screens/auth/password_reset_screen.dart';
import 'package:flutter_sample/src/screens/auth/register/register_confirm_screen.dart';
import 'package:flutter_sample/src/screens/auth/register/register_input_screen.dart';
import 'package:flutter_sample/src/screens/home/home_detail_screen.dart';
import 'package:flutter_sample/src/screens/home/home_screen.dart';
import 'package:flutter_sample/src/screens/setting/setting_account_edit_screen.dart';
import 'package:flutter_sample/src/screens/setting/setting_phone_number_edit_screen.dart';
import 'package:flutter_sample/src/screens/setting/setting_screen.dart';
import 'package:flutter_sample/src/screens/setting/setting_withidraw_screen.dart';
import 'package:flutter_sample/src/screens/splash_screen.dart';
import 'package:flutter_sample/src/themes/app_theme.dart';
import 'package:provider/provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
runApp(ChangeNotifierProvider(
create: (conntext) => AuthProvider(),
child: const MyApp(),
));
}
// ルート定義
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
initialRoute: '/splash',
routes: {
'/splash': (context) => const SplashScreen(),
...
}
);
}
}
screens/splash_screen.dart
スプラッシュ画面では、アニメーションとともに認証情報をチェックし、AuthWrapper
に遷移します。
import 'dart:async';
import 'package:flutter_sample/src/utils/route_utils.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_sample/src/providers/auth_provider.dart';
// スプラッシュ画面
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
int _counter = 4;
double _opacity = 0.0;
void initState() {
super.initState();
// AnimationControllerの初期化
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_controller.forward();
_startSplashSequence();
}
// スプラッシュ画面のシーケンス処理
Future<void> _startSplashSequence() async {
while (_counter > 0) {
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
setState(() {
_counter--;
_opacity = _counter == 3 ? 0.5 : 1.0;
});
}
}
// 認証状態をチェックして画面遷移
await _checkAuthAndNavigate();
}
// 認証状態をチェックして画面遷移
Future<void> _checkAuthAndNavigate() async {
// mountedチェックしてからcontextを使ってProviderを呼び出す
if (!mounted) return;
final authProvider = Provider.of<AuthProvider>(context, listen: false);
await authProvider.checkAuthentication();
// 再度mountedチェックしてから画面遷移
if (mounted) {
RouteUtils.navigateToAuthWrapper(context);
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedOpacity(
opacity: _opacity,
duration: const Duration(seconds: 1),
child: const Text(
'Flutter Sample',
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}
AuthProvider
による認証チェックAPIの呼び出し
AuthProvider
のcheckAuthentication
メソッドでは、isAuthenticated
の確認と設定行い、isAuthenticated
がtrue
なら、バックエンドのAPIに対して二段階認証確認リクエストを送信し、その結果に基づいて認証状態を更新します。以下はその抜粋です。
Future<void> checkAuthentication() async {
_isAuthenticated = await _authService.isAuthenticated();
if (_isAuthenticated) {
try {
_isTwoAuthenticated = await _authService.isTwoAuthenticated();
} on ApiException catch (e) {
_isAuthenticated = false;
_isTwoAuthenticated = false;
} catch (e) {
_isAuthenticated = false;
_isTwoAuthenticated = false;
}
}
notifyListeners();
}
checkAuthentication
メソッド内で、isAuthenticated
、isTwoAuthenticated
確認及び、設定を行います。その後、AuthWrapper
で認証状態をチェックし、適切な画面に遷移します。
認証状態に応じた画面切り替え
AuthWrapper
ウィジェットは、ユーザーの認証状態に基づいて異なる画面へ遷移させる役割を持っています。例えば、認証されていない場合はログイン画面を表示し、認証済みであればメイン画面、二段階認証が未完了の場合は二段階認証画面を表示します。
widgets/auth_wrapper.dart
以下は、AuthWrapper
の実装コードです。AuthProvider
から現在の認証状態を取得し、isAuthenticated
およびisTwoAuthenticated
の値に基づいて画面を切り替えます。
import 'package:flutter_sample/src/providers/auth_provider.dart';
import 'package:flutter_sample/src/screens/auth/login_screen.dart';
import 'package:flutter_sample/src/screens/auth/two_factor_screen.dart';
import 'package:flutter_sample/src/screens/main_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 認証状態に応じて画面を切り替えるウィジェット
class AuthWrapper extends StatelessWidget {
const AuthWrapper({super.key});
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context);
if (authProvider.isAuthenticated) {
return authProvider.isTwoAuthenticated
? const MainScreen() // ログインしている場合
: const TwoFactorScreen(); // 二要素認証が必要な場合
} else {
return LoginScreen(); // ログイン画面
}
}
}
AuthProvider
認証状態を保持するAuthProvider
では、以下の3つのプロパティを使って認証状態を管理しています。
-
isAuthenticated
:認証済みかどうかを示すブール値 -
isTwoAuthenticated
:二段階認証が完了しているかどうかを示すブール値 -
message
:エラーメッセージなどを格納するオプションの文字列
class AuthProvider with ChangeNotifier {
final AuthService _authService = AuthService(); // 認証サービスを使用
bool _isAuthenticated = false;
bool _isTwoAuthenticated = false;
String? _message;
bool get isAuthenticated => _isAuthenticated;
bool get isTwoAuthenticated => _isTwoAuthenticated;
String? get message => _message;
// 認証や二段階認証の処理をここで行う
...
}
認証フローの流れ
以下のフロー図は、AuthWrapper
で認証状態に応じた画面切り替えの処理を表しています。認証状態によってメイン画面やログイン画面、二段階認証画面に遷移します。
ログイン画面
LoginScreen
では、ユーザーがログイン情報を入力し、ログインボタンを押すことでログイン処理を実行します。ログインが成功すれば、AuthWrapper
に遷移し、認証状態に応じて適切な画面が表示されます。ここでは、その流れを説明します。
screens/auth/login_screen.dart
ログイン処理の流れ
ユーザーがメールアドレスとパスワードを入力後、ログインボタンを押すことで、以下のフローでログイン処理が進行します。
- 入力値のバリデーションが行われます。
- ローディングダイアログを表示し、
authProvider.login
メソッドで認証APIを呼び出します。 - 認証が成功すれば、
isAuthenticated = true
およびisTwoAuthenticated = false
に設定され、認証完了状態になります。 - 認証が失敗した場合は、エラーメッセージが表示されます。
- 成功時には
AuthWrapper
に遷移し、さらに認証状態に基づいて画面が切り替わります(例: 二段階認証が必要な場合はTwoFactorScreen
が表示される)。
ログイン処理のコード
以下は、login_screen.dart
におけるログイン処理のコードです。
void login() async {
// バリデーション
final isValid = _formKey.currentState!.validate();
if (!isValid) return;
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final email = _emailController.text;
final password = _passwordController.text;
// ローディングダイアログを表示
DialogUtils.showLoadingDialog(context);
// ログインメソッドを呼び出す
await authProvider.login(email, password);
if (!context.mounted) return;
// ローディングダイアログを非表示
DialogUtils.hideLoadingDialog(context);
// メッセージがあれば、エラーメッセージを表示
if (authProvider.message != null) {
SnackbarUtils.showSnackbar(context, authProvider.message!);
// メッセージがなければ、AuthWrapperに遷移。
} else {
RouteUtils.navigateToAuthWrapper(context);
}
}
AuthProvider
によるログインAPIの呼び出し
AuthProvider
のlogin
メソッドでは、バックエンドのAPIに対して認証リクエストを送信し、その結果に基づいて認証状態を更新します。以下はその抜粋です。
// ログイン処理
Future<void> login(String email, String password) async {
try {
_message = null;
await _authService.login(email, password); // ログインAPIを実行
_isAuthenticated = true; // 認証成功時
_isTwoAuthenticated = false; // 二段階認証はまだ完了していない
} on ApiException catch (e) {
_message = e.message; // APIからのエラーメッセージを格納
} catch (e) {
_message = 'ログインに失敗しました'; // 一般的なエラーメッセージ
}
}
login
メソッド内で、認証が成功するとisAuthenticated
がtrue
になり、二段階認証が未完了の状態としてisTwoAuthenticated
はfalse
に設定されます。その後、AuthWrapper
で認証状態をチェックし、TwoFactorScreen
に遷移します。
認証フロー図
以下のフロー図は、LoginScreen
から認証処理が実行され、認証成功時にAuthWrapper
に遷移する流れを視覚的に表しています。
登録処理の流れ
-
ログイン画面からの遷移
ユーザーは、ログイン画面の「会員登録がまだの方はこちら」をタップして、RegisterInputScreen
に遷移します。login_screen.dart(抜粋)CommonNavigatorText( onTap: () { Navigator.pushNamed(context, '/register/input'); }, text: '会員登録がまだの方はこちら', ),
-
登録情報入力
RegisterInputScreen
でユーザーは登録情報を入力し、Confirm
ボタンを押すことで、navigateToRegisterConfirmScreen
関数が実行されます。この関数では、重複するメールアドレスのチェックを行い、問題がなければRegisterConfirmScreen
に遷移します。
void navigateToRegisterConfirmScreen () async {
final isValid = _formKey.currentState!.validate();
if (!isValid) return;
DialogUtils.showLoadingDialog(context);
try {
// ユーザーの存在チェック
await authService.checkUser(_emailController.text);
if (!mounted) return;
DialogUtils.hideLoadingDialog(context);
Navigator.of(context).pushNamed('/register/confirm', arguments: {
'name': _nameController.text,
'email': _emailController.text,
'phone_number': _phoneNumberController.text,
'password': _passwordController.text,
});
} on ApiException catch (e) {
DialogUtils.hideLoadingDialog(context);
SnackbarUtils.showSnackbar(context, e.message);
} catch (e) {
DialogUtils.hideLoadingDialog(context);
SnackbarUtils.showSnackbar(context, 'エラーが発生しました');
}
}
-
登録内容の確認
RegisterConfirmScreen
では、入力された情報が表示され、Register
ボタンを押すことでregister
関数が実行されます。この関数では、登録APIを呼び出して、成功すれば認証情報を更新し、AuthWrapper
に遷移します。
void register() async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final name = _nameController.text;
final email = _emailController.text;
final phoneNumber = _phoneNumberController.text;
final password = _passwordController.text;
// ローディングダイアログを表示
DialogUtils.showLoadingDialog(context);
// 登録メソッドを呼び出す
await authProvider.register(name, email, phoneNumber, password);
if (!context.mounted) return;
// エラーメッセージがある場合は、SnackBar で表示
if (authProvider.message != null) {
SnackbarUtils.showSnackbar(context, authProvider.message!);
} else {
RouteUtils.navigateToAuthWrapper(context);
}
}
register
関数内の登録APIが問題なければ、isAuthenticated = true
、isTwoAuthenticated = false
にして、AuthWrapperに遷移する。
AuthWrapperで認証情報を確認して、TwoFactorScreenに遷移します。
Future<void> register(String name, String email, String phoneNumber, String password) async {
try {
_message = null;
await _authService.register(name, email, phoneNumber, password);
_isAuthenticated = true;
_isTwoAuthenticated = false;
} on ApiException catch (e) {
_message = e.message;
} catch (e) {
_message = '登録に失敗しました';
}
}
登録フロー図
以下のフロー図は、LoginScreen
から登録処理が実行され、登録成功時にAuthWrapper
に遷移する流れを視覚化的に表しています。
二段階認証画面
TwoFactorScreen
では、ユーザーが4桁の認証コードを入力し、二段階認証するボタンを押すことで二段階認証処理を実行します。二段階認証が成功すれば、AuthWrapper
に遷移し、認証状態に応じて適切な画面が表示されます。ここでは、その流れを説明します。
screens/auth/two_factor_screen.dart
二段階認証処理の流れ
ユーザーが4桁の認証コードを入力後、二段階認証するボタンを押すことで、以下のフローで二段階処理が進行します。
- ローディングダイアログを表示し、
authProvider.twoFactorAuth
メソッドで二段階認証APIを呼び出します。 - 認証が成功すれば、
isTwoAuthenticated = true
に設定され、認証完了状態になります。 - 認証が失敗した場合は、エラーメッセージが表示されます。
- 成功時には
AuthWrapper
に遷移し、さらに認証状態に基づいて画面が切り替わります。
二段階認証処理のコード
以下は、two_factor_screen.dart
における二段階認証処理のコードです。
void twoFactorAuth() async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
DialogUtils.showLoadingDialog(context);
await authProvider.twoFactorAuth(passcode);
if (!mounted) return;
DialogUtils.hideLoadingDialog(context);
if (authProvider.message != null) {
SnackbarUtils.showSnackbar(context, authProvider.message!);
} else {
RouteUtils.navigateToAuthWrapper(context);
}
}
AuthProvider
による二段階認証APIの呼び出し
AuthProvider
のtwoFactorAuth
メソッドでは、バックエンドのAPIに対して二段階認証リクエストを送信し、その結果に基づいて認証状態を更新します。以下はその抜粋です。
Future<void> twoFactorAuth(String passcode) async {
try {
_message = null;
await _authService.twoFactorAuth(passcode);
_isTwoAuthenticated = true;
} on ApiException catch (e) {
_message = e.message;
} catch (e) {
_message = '認証に失敗しました';
}
}
twoFactorAuth
メソッド内で、認証が成功するとisTwoAuthenticated
がtrue
に設定されます。その後、AuthWrapper
で認証状態をチェックし、MainScreen
に遷移します。
二段階認証フロー図
以下のフロー図は、TwoFactorScreen
から二段階認証処理が実行され、認証成功時にAuthWrapper
に遷移する流れを視覚的に表しています。
まとめ
今回、初めてFlutterを触りながら、認証機能を実装した内容をまとめました。
次回は、MainScreenの周りの実装について詳しく説明する予定です。
Flutterに詳しい方や初心者の方からのアドバイスも大歓迎ですので、ぜひコメントいただけると嬉しいです!
Discussion