初めてFlutterを触ってみた- Flutter × Laravel 認証- ②
初めてFlutterを触ってみた②
前回の記事の続きになります。
初めてFlutterを触ってみた- Flutter × Laravel 認証- ①
認証後のMainScreen周りの説明をします。
MainScreen周りのフロー図
以下のフロー図はAuthWrapper
から認証・二段階認証済でMainScreen
から各画面の遷移の流れを抜粋して表したものです。
メイン画面
MainScreen
では、ホーム (HomeScreen) と設定 (SettingScreen) という2つのタブで構成され、ユーザーは下部のナビケーションバーを使ってタブを切り替えることができます。
screens/main_screen.dart
全体の流れ
- 画面初期化: MainScreenが表示されると、初期の画面(initialIndexに基づく)が表示されます。
- ナビゲーションバー操作: ナビゲーションバーのアイコンをタップすると
_onItemTapped
が呼ばれ、currentIndexが更新されます。 - 画面切り替え: タブのインデックスに応じて、ホーム画面や設定画面に切り替わります。
- ドロワーメニュー: 右側のメニュー(ドロワー)を開くことで、追加のナビゲーションオプションが提供されます。
以下は、MainScreen
の実装コードです。
import 'package:flutter_sample/src/screens/home/home_screen.dart';
import 'package:flutter_sample/src/screens/setting/setting_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sample/src/widgets/common_app_bar.dart';
import 'package:flutter_sample/src/widgets/common_drawer.dart';
import 'package:flutter_sample/src/widgets/common_navigation_bar.dart';
// 認証後のメイン画面
class MainScreen extends StatefulWidget {
final int initialIndex;
const MainScreen({
super.key,
this.initialIndex = 0,
});
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
late int currentIndex;
void initState() {
super.initState();
currentIndex = widget.initialIndex;
}
// メニューWidgetリスト
final List<Widget> _screens = [
const HomeScreen(),
SettingScreen(),
];
void _onItemTapped(int index) {
setState(() {
currentIndex = index;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: const CommonAppBar(),
endDrawer: const CommonDrawer(),
body: _screens[currentIndex],
bottomNavigationBar: CommonNavigationBar(onTap: _onItemTapped, currentIndex: currentIndex),
);
}
}
MainScreenで使用してるWidgetsについて
widgets/common_app_bar.dart
共通して使用されるアプリバーです。titleを指定しないときはFlutter Sample
を表示します。
以下は、CommonAppBar
の実装コードです。
import 'package:flutter/material.dart';
// 共通のアプリバー
class CommonAppBar extends StatelessWidget implements PreferredSizeWidget {
const CommonAppBar({
super.key,
this.title,
});
final String? title;
Widget build(BuildContext context) {
return AppBar(
title: Text(
title ?? 'Flutter Sample', // title が null の場合は 'Flutter Sample' を表示
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
backgroundColor: Colors.pink,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
);
}
Size get preferredSize => const Size.fromHeight(50);
}
widgets/common_drawer.dart
共通して使用されるナビゲーションドロワー(スライドメニュー)です。
このドロワーでは、認証後のログアウト機能を実装しています。
以下は、CommonDrawer
の実装コードです。
import 'package:flutter_sample/src/providers/auth_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sample/src/utils/dialog_utils.dart';
import 'package:flutter_sample/src/utils/route_utils.dart';
import 'package:flutter_sample/src/utils/snackbar_utils.dart';
import 'package:provider/provider.dart';
// ドロワーの共通部分
class CommonDrawer extends StatefulWidget {
const CommonDrawer({ super.key });
State<CommonDrawer> createState() => _CommonDrawerState();
}
class _CommonDrawerState extends State<CommonDrawer> {
void logout () async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
DialogUtils.showLoadingDialog(context);
await authProvider.logout();
if (!mounted) return;
DialogUtils.hideLoadingDialog(context);
if (authProvider.message != null) {
SnackbarUtils.showSnackbar(context, authProvider.message!);
} else {
RouteUtils.navigateToAuthWrapper(context);
}
}
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: Drawer(
child: ListView(
children: <Widget>[
DrawerHeader(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 5),
decoration: const BoxDecoration(
color: Colors.pink,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: Text(
'Common Drawer',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context);
},
);
},
)
],
),
),
ListTile(
title: const Text('Logout'),
onTap: () {
logout();
},
),
],
)
)
);
}
}
ログアウト処理の流れ
ユーザーがログアウトボタンを押すことで、以下のフローでログアウト処理が進行します。
- ローディングダイアログを表示し、
authProvider.logout
メソッドでログアウトAPIを呼び出します。 - ログアウトが成功すれば、
isAuthenticated = false
および、isTwoAuthenticated = false
に設定され、ログアウト完了状態になります。 - 認証が失敗した場合は、エラーメッセージが表示されます。
- 成功時には
AuthWrapper
に遷移し、さらに認証状態に基づいて画面が切り替わります。
以下は、common_drawer.dart
におけるログアウト処理のコードです。
void logout () async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
DialogUtils.showLoadingDialog(context);
await authProvider.logout();
if (!mounted) return;
DialogUtils.hideLoadingDialog(context);
if (authProvider.message != null) {
SnackbarUtils.showSnackbar(context, authProvider.message!);
} else {
RouteUtils.navigateToAuthWrapper(context);
}
}
AuthProvider
によるログアウトAPIの呼び出し
AuthProvider
のlogout
メソッドでは、バックエンドのAPIに対してログアウトリクエストを送信し、その結果に基づいて認証状態を更新します。以下はその抜粋です。
// ログアウト処理
Future<void> logout() async {
try {
_message = null;
await _authService.logout();
_isAuthenticated = false;
_isTwoAuthenticated = false;
} on ApiException catch (e) {
_message = e.message;
} catch (e) {
_message = 'ログアウトに失敗しました';
}
}
logout
メソッド内で、ログアウトが成功するとisAuthenticated
がfalse
、isTwoAuthenticated
もfalse
に設定されます。その後、AuthWrapper
で認証状態をチェックし、LoginScreen
に遷移します。
ログアウトフロー図
以下のフロー図は、CommonDrawer
からログアウト処理が実行され、ログアウト成功時にAuthWrapper
に遷移する流れを視覚的に表しています。
widgets/common_navigation_bar.dart
共通のボトムナビゲーションバー(画面下に表示されるメニュー)です。
受け取ったonTapの処理とcurrentIndexの情報を元に該当タブのアイコンの色を適切に表示します。
以下は、CommonNavigationBar
の実装コードです。
import 'package:flutter/material.dart';
// 共通のナビゲーションバー
class CommonNavigationBar extends StatefulWidget {
const CommonNavigationBar({
super.key,
required this.onTap,
this.currentIndex = 0,
});
final ValueChanged<int> onTap;
final int currentIndex;
State<CommonNavigationBar> createState() => _CommonNavigationBarState();
}
class _CommonNavigationBarState extends State<CommonNavigationBar> {
// BottomNavigationBar のアイテムリスト
final List<BottomNavigationBarItem> _items = const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Setting',
),
];
Widget build(BuildContext context) {
return BottomNavigationBar(
items: _items,
currentIndex: widget.currentIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.pink,
unselectedItemColor: Colors.grey,
showSelectedLabels: true,
showUnselectedLabels: true,
onTap: (index) {
widget.onTap(index);
},
);
}
}
MainScreenのbodyにしてる各画面
ホーム画面
HomeScreen
では、リストアイテムの取得、表示、及びページネーションを管理します。
screens/home/home_screen.dart
ホーム画面の処理の流れ
-
initState
メソッド内でスクロールリスナーを追加し、初回のデータ取得APIを呼び出します。 - 取得が成功すれば、リストにアイテムを追加して、リスト形式でアイテムを表示します。
- ユーザーがリストの最下部に到達すると、次のページのデータ取得APIを呼び出します。取得に成功すれば、リストに追加します。
- アイテムをタップすることで詳細画面が表示されます。
変数の定義
以下は、home_screen.dart
における変数の定義です。
HomeService homeService = HomeService();
bool _isLoading = false;
bool _isLoadingMore = false;
List<Item> data = [];
final ScrollController _scrollController = ScrollController();
int currentPage = 1;
- homeService: HomeServiceクラスのインスタンス。APIからデータを取得するために使用します。
- _isLoading: 初回データ取得中かどうかを示すフラグ。
- _isLoadingMore: 追加データ取得中かどうかを示すフラグ。
- data: 取得したItemオブジェクトのリスト。
- _scrollController: スクロールイベントを監視するためのコントローラー。
- currentPage: 現在のページ番号。データのページネーションに使用。
データ取得処理のコード
-
_isLoading
をtrue
にして、データ取得中であることを示します。 - バックエンドのAPIに対してアイテム取得リクエストを送信し、現在のページに応じたデータを取得します。
- データ取得に成功したら、
newData
を既存のdata
に追加、currentPage
を1つ進め、_isLoading
をfalse
にして、データ取得終了であることを示します。 - データ取得に失敗した場合は、
_isLoading
をfalse
にして、データ取得終了であることを示します。
以下は、home_screen.dart
におけるデータ取得処理のコードです。
Future<void> _fetchData() async {
setState(() {
_isLoading = true;
});
try {
// サービスクラスから新しいデータを取得し、リストに追加
List<Item> newData = await homeService.getItems(currentPage);
if (!mounted) return;
setState(() {
data.addAll(newData); // 新しいデータを既存のリストに追加
currentPage++; // ページを進める
_isLoading = false;
});
} on ApiException catch (_) {
setState(() {
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
screens/home/home_detail.dart
詳細画面の流れ
-
HomeScreen
でアイテムカードのIconButton
をタップすると、BottomSheetUtils.showBottomSheet
メソッドが呼び出され、HomeDetailScreen
がボトムシートに表示されます。home_screen.dart(抜粋)IconButton( onPressed: () { BottomSheetUtils.showBottomSheet( context, HomeDetailScreen(id: id), ); }, icon: const Icon(Icons.arrow_forward_ios), )
- HomeDetailScreenの表示時に、
initState
メソッドを呼び出し、詳細データ取得APIを呼び出します。 - データ取得中はローディングスピナーを表示します。
- データ取得に成功したら、取得したデータを画面に表示します。
- ユーザーはボトムシートの閉じるボタンをタップすることで、ボトムシートを閉じることができます。
変数の定義
以下はhome_detail_screen.dart
における変数の定義です。
HomeService homeService = HomeService();
bool _isLoading = false;
Item _data = Item(id: 0, title: '', description: '', image: '');
- homeService: HomeServiceクラスのインスタンス。APIからデータを取得するために使用します。
- _isLoading: 初回データ取得中かどうかを示すフラグ。
- _data: APIから取得したアイテムのデータを格納する変数。初期値として空のアイテムオブジェクトを設定しています。
データ取得処理のコード
-
_isLoading
をtrue
にして、データ取得中であることを示します。 - バックエンドのAPIに対して、指定されたIDのアイテム取得リクエストを送信し、アイテム詳細を取得します。
- データ取得に成功したら
_data
に格納し、_isLoading
をfalse
にして、データ取得終了であることを示します。 - データ取得に失敗した場合は、
_isLoading
をfalse
にして、データ取得終了であることを示します。
以下は、home_detail_screen.dart
におけるデータ取得処理のコードです。
void _fetchData() async {
setState(() {
_isLoading = true;
});
try {
_data = await homeService.getItemDetail(widget.id);
setState(() {
_isLoading = false;
});
} on ApiException catch (_) {
setState(() {
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
Setting画面
SettingScreen
では、アカウントにまつわるメニューの一覧を表示します。
screens/setting/setting_screen.dart
全体の流れ
-
_items
に定義されているメニューをリスト形式で表示します。 - リストをタップすると定義している画面に遷移します。
以下は、setting_screen.dart
の実装コードです。
import 'package:flutter/material.dart';
// 設定画面
class SettingScreen extends StatelessWidget {
SettingScreen({super.key});
final List _items = [
{
'title': 'アカウント情報変更',
'route': '/setting/account/edit',
},
{
'title': '電話番号変更',
'route': '/setting/phone/edit',
},
{
'title': '退会',
'route': '/setting/withdraw',
}
];
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.all(10),
child: Center(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
return Card(
color: Colors.grey[100],
child: ListTile(
title: Center(child: Text(_items[index]['title'], style: const TextStyle(fontWeight: FontWeight.bold))),
onTap: () {
Navigator.of(context).pushNamed(_items[index]['route']);
},
)
);
},
),
),
)
);
}
}
Setting画面から遷移する各画面
screens/setting/setting_account_edit_screen.dart
アカウント情報変更画面です。ユーザーのメールアドレスとパスワードの変更ができます。
アカウント情報変更処理の流れ
ユーザーが新しいメールアドレスとパスワードを入力後、変更ボタンを押すことで、以下のフローでアカウント情報変更処理が進行します。
- 入力値のバリデーションが行われます。
- ローディングダイアログを表示し、アカウント情報変更APIを呼び出します。
- アカウント情報変更処理が成功すれば、「アカウント情報を更新しました」の成功メッセージを表示して、
SettingScreen
に戻ります。 - アカウント情報変更処理が失敗した場合は、エラーメッセージが表示されます。
アカウント情報変更処理のコード
以下は、setting_account_edit_screen.dart
におけるアカウント情報変更処理のコードです。
void updateAccountInfo() async {
final isValid = _formKey.currentState!.validate();
if (!isValid) return;
// ローディングダイアログを表示
DialogUtils.showLoadingDialog(context);
try {
await settingService.updateUser(_emailController.text, _passwordController.text);
if (!mounted) return;
SnackbarUtils.showSnackbar(context, 'アカウント情報を更新しました');
DialogUtils.hideLoadingDialog(context);
Navigator.of(context).pop();
} on ApiException catch (e) {
DialogUtils.hideLoadingDialog(context);
SnackbarUtils.showSnackbar(context, e.message);
} catch (e) {
DialogUtils.hideLoadingDialog(context);
SnackbarUtils.showSnackbar(context, 'アカウント情報の更新に失敗しました');
}
}
screens/setting/setting_phone_number_edit_screen.dart
電話番号変更画面です。二段階認証で使用する電話番号の変更ができます。
電話番号変更処理の流れ
ユーザーが新しい電話番号を入力後、変更ボタンを押すことで、以下のフローで電話番号変更処理が進行します。
- 入力値のバリデーションが行われます。
- ローディングダイアログを表示し、電話番号変更APIを呼び出します。
- 電話番号変更処理が成功すれば、
authProvider.successPhoneNumber
メソッドを呼び出します。 -
isAuthenticated = true
およびisTwoAuthenticated = false
に設定され、認証済、二段階認証未認証状態になります。 - 電話番号変更処理が失敗した場合は、エラーメッセージが表示されます。
- 電話番号変更処理成功時には、「電話番号を変更しました」の成功メッセージを表示して、
AuthWrapper
に遷移し、認証情報に基づいて画面が切り替わります。
電話番号変更処理のコード
以下は、setting_phone_number_edit_screen.dart
における電話番号変更処理のコードです。
Future<void> updatePhoneNumber() async {
final isValid = _formKey.currentState!.validate();
if (!isValid) return;
final authProvider = Provider.of<AuthProvider>(context, listen: false);
DialogUtils.showLoadingDialog(context);
try {
await settingService.updatePhoneNumber(_phoneNumberController.text);
await authProvider.successPhoneNumber();
if (!mounted) return;
SnackbarUtils.showSnackbar(context, '電話番号を変更しました');
DialogUtils.hideLoadingDialog(context);
RouteUtils.navigateToAuthWrapper(context);
} on ApiException catch (e) {
DialogUtils.hideLoadingDialog(context);
SnackbarUtils.showSnackbar(context, e.message);
} catch (e) {
SnackbarUtils.showSnackbar(context, '電話番号の変更に失敗しました');
DialogUtils.hideLoadingDialog(context);
}
}
電話番号変更処理成功時のAuthProvider
による認証状態の変更
AuthProvider
のsuccessPhoneNumber
メソッドでは、認証状態を更新します。以下はその抜粋です。
Future<void> successPhoneNumber() async {
_isAuthenticated = true;
_isTwoAuthenticated = false;
}
successPhoneNumber
メソッドが呼び出されると、isAuthenticated
がtrue
に、二段階認証が未完了の状態としてisTwoAuthenticated
はfalse
に設定されます。その後、AuthWrapper
で認証状態をチェックし、TwoFactorScreen
に遷移します。
電話番号変更フロー図
以下のフロー図は、SettingPhoneNumberEditScreen
から電話番号変更処理が実行され、電話番号変更処理成功時にAuthWrapper
に遷移する流れを視覚的に表しています。
screens/setting/setting_withdraw_screen.dart
退会画面です。ユーザーの退会ができます。
退会処理の流れ
ユーザーが退会ボタンを押すことで、以下のフローで退会処理が進行します。
- ローディングダイアログを表示し、退会APIを呼び出します。
- 退会処理が成功すれば、
authProvider.successWithdraw
メソッドを呼び出します。 -
isAuthenticated = false
およびisTwoAuthenticated = false
に設定され、未認証、二段階認証未認証状態になります。 - 退会処理が失敗した場合は、エラーメッセージが表示されます。
- 退会処理成功時には、「退会しました」の成功メッセージを表示して、
AuthWrapper
に遷移し、認証情報に基づいて画面が切り替わります。
退会処理のコード
以下は、setting_withdraw_screen.dart
における退会処理のコードです。
Future<void> withidraw() async {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
DialogUtils.showLoadingDialog(context);
// 退会処理
try {
await settingService.withdraw();
await authProvider.successWithidraw();
if (!mounted) return;
SnackbarUtils.showSnackbar(context, '退会しました');
DialogUtils.hideLoadingDialog(context);
RouteUtils.navigateToAuthWrapper(context);
} on ApiException catch (e) {
SnackbarUtils.showSnackbar(context, e.message);
DialogUtils.hideLoadingDialog(context);
} catch (e) {
SnackbarUtils.showSnackbar(context, '退会に失敗しました');
DialogUtils.hideLoadingDialog(context);
}
}
退会処理成功時のAuthProvider
による認証状態の変更
AuthProvider
のsuccessWithdraw
メソッドでは、認証状態を更新します。以下はその抜粋です。
Future<void> successWithidraw() async {
_isAuthenticated = false;
_isTwoAuthenticated = false;
}
successWithdraw
メソッドが呼び出されると、isAuthenticated
がfalse
に、二段階認証が未完了の状態としてisTwoAuthenticated
はfalse
に設定されます。その後、AuthWrapper
で認証状態をチェックし、LoginScreen
に遷移します。
退会フロー図
以下のフロー図は、SettingWithdrawScreen
から退会処理が実行され、退会処理成功時にAuthWrapper
に遷移する流れを視覚的に表しています。
まとめ
今回は、MainScreen周りの実装についてまとめました。
次回は、触れていなかったLaravel側との連携について説明する予定です。
Flutterに詳しい方や初心者の方からのアドバイスも大歓迎ですので、ぜひコメントいただけると嬉しいです!
Discussion