LoginSignup
3
4

概要

約半年間、独学でFlutterを勉強しました。
自分なりに開発の型が見えてきたのでGitのリポジトリと記事を残したいと思います。
本記事はそのPresentation層編です。



flutterプロジェクトの開始時に作成されるサンプルアプリを置き換えたもので、基本的な仕様やUIは一緒ですが、カウントを1つ進めるたびsharedPreferencesに現在のカウントが保存されるようになっています。


デザインパターン

こちらの大変わかりやすい記事を参考にさせていただきました!

レイヤードアーキテクチャに関しては、こちらの記事内で詳しく(かつ非常にわかりやすく!)説明がされているので、こちらをご参照ください。



ディレクトリ構成

lib
├ features/
│ ├ common_utils/
│ └ #feature-1
│ ├ application/
│ ├ domain/
│ └ infrastructure/
presentation/
│ ├ constants/
│ ├ page/
│ ├ router/
│ └ snackbar/
└ main.dart


constants

アプリ内で使用するcolorやthemeが置いてあります。

\lib\presentation\constants\brand_color.dart
/// presentation層で使用する色を定義するクラス
class BrandColor {
  //appのメインカラー
  static const mainColor = Color(0xFFF4B400);

  //文字色
  static const mainTextColor = Colors.black54;
  static const subTextColor = Colors.black45;

  //loadingの背景色
  static const loadingBgColor = Color.fromRGBO(0, 0, 0, 0.2);
}

アプリの基調となる色や文字色をここにまとめています。

・ScreenUtils

概要編でも触れましたが、このアプリではScreenUtilsというデバイスのサイズに合わせて文字サイズや縦横の数値を柔軟に変更してくれるパッケージを導入しています。

こちらのパッケージを導入すると、double型にh, w, spなどのメソッドが新たに追加されます。

\lib\presentation\page\common_components\small_text.dart
class SmallText extends StatelessWidget {
  final String text;
  const SmallText({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(
-      fontSize: RawInt.p10,
+      fontSize: RawInt.p10.sp, 
       color: BrandColor.subTextColor,
      ),
    );
  }
}

…としてあげることで、デバイスやブラウザの大きさに合わせていい感じに値が変わってくれます。
ちなみにRawIntは数値を保持しているクラスです(intでなくdoubleを持っていますね…)。

\lib\presentation\constants\size.dart
/// 値の意味を含まないサイズ一覧定義
class RawInt {
  static const p0 = 0.0;
  static const p1 = 1.0;
  static const p2 = 2.0;
  static const p4 = 4.0;
  static const p6 = 6.0;
  
  ...
}

widgetの横幅を指定する際にはwを、縦幅はhを、文字サイズやアイコンサイズはspを使用します。

page

各ページのWidgetやWidgetで使用するStringなどを配置しています。

common_components

各ページに共通して使用されるWidgetを配置しています。

・gap

ウィジェット同士の間隔を調整するウィジェットを生成するクラスです

\lib\presentation\page\common_components\gap.dart
// widget間の隙間をつくるwidgetを定義するクラス
// 縦の隙間はh, 横の隙間はw
class Gap extends StatelessWidget {
  const Gap._({
    required this.width,
    required this.height,
  });

  final double width;
  final double height;

  /// W
  factory Gap.w(double width) {
    return Gap._(width: width, height: 0);
  }

  /// H
  factory Gap.h(double height) {
    return Gap._(width: 0, height: height);
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(width: width, height: height);
  }
}

引数にdoubleを渡し、このように使用します

\lib\presentation\page\counter\counter_page.dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    const BigText(text: countUpDescriptionText),
    //BigTextとSmallTextの縦の隙間を埋めるwidget
    Gap.h(RawInt.p32.h),
    SmallText(text: count.value.toString()),
  ],
),

BigTextとSmallTextの間に間隔が生まれました!

各featureのディレクトリ(このテンプレートではcounter)

各featureに対応したpageやpage内で使用されるTextを配置しています

router

main.dartから呼び出されるapp.dartもこの階層に配置しています。

・app.dart

\lib\presentation\router\app.dart
class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    //router
    final router = ref.watch(goRouterProvider);
    //theme
    final ThemeData themeData = ref.read(themeProvider);

    return MaterialApp(
        //ページをタップするとfocusを外す
        home: GestureDetector(
            onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
            //loading画面をメインの画面に重ねる
            child: Stack(children: [
              MaterialApp.router(
                //以下の階層でthemeが適用される
                theme: themeData,
                routerConfig: router,
              ),
              const LoadingScreen(),
            ])));
  }
}

MaterialApp.routerとすることで、以下の階層でGoRouterによる画面遷移が実行できます。
MaterialAppとLoadingScreen()をStackすることで、半透明な画面のロード画面を重ねています。
また、ここをGestureDetectorでラップしてあげることによって画面の何もないところをタップするとキーボードを閉じる機能を与えてあげています。

・page_path.dart

ページの遷移はGoRouterを導入しています。

pageの情報を持たせたenumを用意してあげます。

\lib\presentation\router\page_path.dart
enum PageId {
  home,
}

/// 設計上の画面パス
extension PagePath on PageId {
  String get path {
    switch (this) {
      case PageId.home:
        return '/';
    }
  }
}

/// 設計上の画面名
extension PageName on PageId {
  String get routeName {
    switch (this) {
      case PageId.home:
        return 'home';
    }
  }
}

PageId.home.pathでString型の'/'を、PageId.home.routeNameでString型の'home'が返却されます。
新たにページを追加したいときはまずこちらのenumを追加したあと下記のコードのroutesにGoroute()を追加してあげてください。

GoRouterもproviderで提供しています。

\lib\presentation\router\go_router.dart
final goRouterProvider = Provider(
  (ref) {
    // GoRouterの設定
    final routes = [
      GoRoute(
        path: PageId.home.path,
        name: PageId.home.routeName,
        builder: (context, state) {
          return const CounterPage();
        },
      ),
    ];

    return GoRouter(
      initialLocation: PageId.home.path,
      debugLogDiagnostics: false,
      routes: routes,
    );
  },
);

routesにリスト形式で各ページをもたせ、GoRouterのroutesに渡してあげます。
initialLocationで指定されたpathを持つページが起動時のページになります。

widget内でpushNamedを呼び出してあげることでページ遷移できます。

 final router = ref.read(goRouterProvider);

 // homeに遷移する
 router.pushNamed(PageId.home.routeName);

snackbar

PresentationMixから呼び出すSnackbarを配置しています。
Mixinに関してはこちらの記事をご覧ください!

おわりに

Presentation層編は以上になります。
以下の記事も併せてよろしくお願いします!

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4