🐺

【Flutter】Navigator2.0のシンプルなテンプレート

2021/10/20に公開

無料で素晴らしい教育資料Flutter ApprenticeのNavigator 2.0の説明を見ている際、
少し複雑と感じたため、土台として最小限に近い抜粋をした内容となります。
一番シンプルな部分をコードで追うことに、お役立ちできれば幸いです。

【Flutter】Navigator2.0のシンプルなテンプレート

はじめに

完成テンプレート

実行結果

  • 画面遷移
    • 初期画面であるスプラッシュスクリーン
    • ↓ 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.dartexport 'profile_screen.dart';を追加
      • my_app_pages.dartstatic String profilePath = '/profile';等を追加
    • navigationディレクトリ
      • app_router.dart
        • build関数のNavigator内pages値のif文でProfileScreen.page()を追加
        • _handlePopPage関数で必要なポップイベント処理を追加
  • 状態マネージャ(ChangeNotifier継承クラス)を追加する場合
    以下、ユーザ情報の設定やアクセスを検知するようなprofile_manager.dartを追加

    • modelsディレクトリ
      • profile_manager.dart:追加したい状態マネージャファイル
      • models.dartexport '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);を追加
    • main.dart
      • 状態マネージャを宣言(例:final _profileManager = ProfileManager();
      • initState関数アプリルータ初期化に追加して渡す
        (例:_appRouter = AppRouter(appStateManager: _appStateManager, profileManager: _profileManager,)
      • build関数のMultiProviderに_profileManager(ChangeNotifier継承クラス)を渡す

Discussion