🐺
【Flutter】Navigator2.0のシンプルなテンプレート
無料で素晴らしい教育資料Flutter ApprenticeのNavigator 2.0の説明を見ている際、
少し複雑と感じたため、土台として最小限に近い抜粋をした内容となります。
一番シンプルな部分をコードで追うことに、お役立ちできれば幸いです。
【Flutter】Navigator2.0のシンプルなテンプレート
はじめに
-
【参考】Flutter Apprentice:公式の学習サイト(無料)
-
実行環境
sdk: ">=2.12.0 <3.0.0"
完成テンプレート
- Git hub
実行結果
- 画面遷移
- 初期画面であるスプラッシュスクリーン
- ↓ 2秒後自動で画面遷移
- 3つのボトムナビゲーションを持つ、ホームスクリーン
- (ボトムナビゲーションで画面遷移可能)
- ↓ フローティングアクションボタンで再初期化(スプラッシュスクリーンに戻る)
- スプラッシュスクリーン
- ↓
- ・・・
ディレクトリ構成
-
lib
ディレクトリ以下の構成-
models
ディレクトリ-
models.dart
:models下ファイルをまとめる用(例:export 'app_state_manager.dart';
) -
app_state_manager.dart
:個別アプリのコード -
my_app_pages.dart
:
-
-
navigation
ディレクトリ-
app_router.dart
:アプリの状態の変化をリッスンし、画面遷移を実施
-
-
screens
ディレクトリ-
screens.dart
:screens下ファイルをまとめる用(例:export 'home_screen.dart';
)
-
-
splash_screen.dart
home_screen.dart
main.dart
-
コード内容
画面や、状態マネージャを追加する場合のコードをコメントとして、残しています。
-
pubspec.yaml
-
provider: ^6.0.0
を追加のみ(※以下抜粋)
-
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
provider: ^6.0.0
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/models.dart';
import 'navigation/app_router.dart';
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _appStateManager = AppStateManager();
// 【状態マネージャ追加案】(ChangeNotifier継承クラス)
// 別途、models/profile_managerを作成
// final _profileManager = ProfileManager();
late AppRouter _appRouter;
void initState() {
/// 【ポイント】
/// initState()使用する前にアプリルーターが初期化
/// 状態マネージャーをリッスンし、状態の変化に基づいて、
/// ページルートのリストを構成し、状態マネージャーを接続
/// 状態が変化すると、ルーターはナビゲーターを新しいページのセット
/// で再構成
_appRouter = AppRouter(
appStateManager: _appStateManager,
// 【状態マネージャ追加案】
// profileManager: _profileManager,
);
super.initState();
}
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => _appStateManager,
),
// 【状態マネージャ追加案】
// ChangeNotifierProvider(
// create: (context) => _profileManager,
// )
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'MyApp',
home: Router(
routerDelegate: _appRouter,
backButtonDispatcher: RootBackButtonDispatcher(),
),
),
);
}
}
app_router.dart
import 'package:flutter/material.dart';
import '../models/models.dart';
import '../screens/screens.dart';
// RouterDelegateにより、
// ルーターがアプリの状態の変化をリッスンしてナビゲーターの構成を再構築
class AppRouter extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey<NavigatorState> navigatorKey;
final AppStateManager appStateManager;
// 【状態マネージャ追加案】
// final ProfileManager profileManager;
AppRouter({
required this.appStateManager,
// 【状態マネージャ追加案】
// required this.profileManager,
}) : navigatorKey = GlobalKey<NavigatorState>() {
appStateManager.addListener(notifyListeners);
// 【状態マネージャ追加案】
// profileManager.addListener(notifyListeners);
}
void dispose() {
appStateManager.removeListener(notifyListeners);
// 【状態マネージャ追加案】
// profileManager.removeListener(notifyListeners);
super.dispose();
}
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
/// 【ポイント】
/// 状態管理によって、表示したいページとしている
/// 「context.read<AppStateManager>().xxx」等の関数で状態を変化させる
/// ことで、以下の判定により画面遷移
if (!appStateManager.isInitialized) SplashScreen.page(),
if (appStateManager.isInitialized)
Home.page(appStateManager.getSelectedTab),
// 【状態マネージャ追加案】
// 何かしらの関数実行にて、クラス変数の状態を変化させ、画面遷移させる
// if (profileManager.didSelectUser)
// 【画面追加案】
// ProfileScreen.page(profileManager.getUser),
],
);
}
/// 【ポイント】
/// ポップイベントの処理
// ユーザーが[戻る]ボタンをタップするか(Android)、
// システムの[戻る]ボタンイベントをトリガーすると実施させることを
// 各画面によって定義可能
bool _handlePopPage(Route<dynamic> route, result) {
if (!route.didPop(result)) {
return false;
}
// 【画面追加案】
// if (route.settings.name == MyAppPages.profilePath) {
// profileManager.tapOnProfile(false);
// }
return true;
}
Future<void> setNewRoutePath(configuration) async => () {};
}
app_state_manager.dart
import 'dart:async';
import 'package:flutter/material.dart';
// タブ管理用
class MyAppTab {
static const int explore = 0;
static const int recipes = 1;
static const int toBuy = 2;
}
// アプリの状態管理クラス
class AppStateManager extends ChangeNotifier {
bool _initialized = false;
int _selectedTab = MyAppTab.explore;
bool get isInitialized => _initialized;
int get getSelectedTab => _selectedTab;
void initializeApp() {
Timer(
const Duration(milliseconds: 2000),
() {
_initialized = true;
notifyListeners();
},
);
}
void goToTab(index) {
_selectedTab = index;
notifyListeners();
}
void unInitializeApp() {
_initialized = false;
notifyListeners();
}
}
splash_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/models.dart';
class SplashScreen extends StatefulWidget {
/// 【ポイント】
/// AppRouterクラスでif文を利用し、画面遷移利用するためのpage定義
static MaterialPage page() {
return MaterialPage(
name: MyAppPages.splashPath,
key: ValueKey(MyAppPages.splashPath),
child: const SplashScreen(),
);
}
const SplashScreen({Key? key}) : super(key: key);
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
void didChangeDependencies() {
super.didChangeDependencies();
/// 【ポイント】
/// 状態マネージャーの関数を利用し、画面遷移誘発
context.read<AppStateManager>().initializeApp();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text('SplashScreen'),
Text('Initializing...'),
],
),
),
);
}
}
home_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/models.dart';
class Home extends StatefulWidget {
/// 【ポイント】
/// AppRouterクラスでif文を利用し、画面遷移利用するためのpage定義
static MaterialPage page(int currentTab) {
return MaterialPage(
name: MyAppPages.home,
key: ValueKey(MyAppPages.home),
child: Home(
currentTab: currentTab,
),
);
}
const Home({
Key? key,
required this.currentTab,
}) : super(key: key);
final int currentTab;
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
static List<Widget> pages = <Widget>[
const Center(child: Text('1')),
const Center(child: Text('2')),
const Center(child: Text('3')),
];
Widget build(BuildContext context) {
return Consumer<AppStateManager>(
builder: (
context,
appStateManager,
child,
) {
return Scaffold(
appBar: AppBar(
title: const Text('MyApp'),
),
body: IndexedStack(
index: widget.currentTab,
children: pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: widget.currentTab,
onTap: (index) {
/// 【ポイント】
/// 状態マネージャーの関数を利用し、画面遷移誘発
context.read<AppStateManager>().goToTab(index);
},
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.explore),
label: '1',
),
BottomNavigationBarItem(
icon: Icon(Icons.book),
label: '2',
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: '3',
),
],
),
floatingActionButton: FloatingActionButton(
/// 【ポイント】
/// 状態マネージャーの関数を利用し、画面遷移誘発
onPressed: context.read<AppStateManager>().unInitializeApp,
),
);
},
);
}
}
models.dart
/// models下ファイルをまとめる用
export 'app_state_manager.dart';
export 'my_app_pages.dart'; // pagesも含める
// 【状態マネージャ追加案】
// export 'profile_manager.dart';
my_app_pages.dart
/// 各Screenファイル(例:home_screen.dart)内で
/// page用の静的メソッドを定義して、適切な一意の識別子を設定する用
class MyAppPages {
static String home = '/';
static String splashPath = '/splash';
// 【画面追加案】
// static String profilePath = '/profile';
}
screens.dart
/// screens下ファイルをまとめる用
export 'home_screen.dart';
export 'splash_screen.dart';
// 【画面追加案】
// export 'profile_screen.dart';
画面や状態マネージャを追加する場合
-
画面を追加する場合
以下、ユーザ情報を表示するようなprofile_screen.dart
を追加-
screens
ディレクトリ-
profile_screen.dart
:追加したい画面クラスファイル -
screens.dart
:export 'profile_screen.dart';
を追加 -
my_app_pages.dart
:static String profilePath = '/profile';
等を追加
-
-
navigation
ディレクトリ-
app_router.dart
:-
build
関数のNavigator内pages値のif文でProfileScreen.page()
を追加 -
_handlePopPage
関数で必要なポップイベント処理を追加
-
-
-
-
状態マネージャ(ChangeNotifier継承クラス)を追加する場合
以下、ユーザ情報の設定やアクセスを検知するようなprofile_manager.dart
を追加-
models
ディレクトリ-
profile_manager.dart
:追加したい状態マネージャファイル -
models.dart
:export 'profile_manager.dart';
を追加
-
-
screens
ディレクトリ- ※何かのファイルにて、ボタン押下時等、何らかの方法で、
profile_manager.dart
の持つ値を変化させる
- ※何かのファイルにて、ボタン押下時等、何らかの方法で、
-
navigation
ディレクトリ-
app_router.dart
:- AppRouterの初期化に追加設定
- 引数にするため
final ProfileManager profileManager;
を追加 - コンストラクタに
required this.profileManager,
を追加 - リスナーとして
profileManager.addListener(notifyListeners);
を追加
- 引数にするため
-
build
関数のNavigator内pages値のif文でprofileManager.xxx
等の追加した状態を利用し、画面遷移を記載 - dispose関数に
profileManager.removeListener(notifyListeners);
を追加
- AppRouterの初期化に追加設定
-
-
main.dart
:- 状態マネージャを宣言(例:
final _profileManager = ProfileManager();
) -
initState
関数アプリルータ初期化に追加して渡す
(例:_appRouter = AppRouter(appStateManager: _appStateManager, profileManager: _profileManager,)
) -
build
関数のMultiProviderに_profileManager
(ChangeNotifier継承クラス)を渡す
- 状態マネージャを宣言(例:
-
Discussion