概要
約半年間、独学で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が置いてあります。
/// 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などのメソッドが新たに追加されます。
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を持っていますね…)。
/// 値の意味を含まないサイズ一覧定義
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
ウィジェット同士の間隔を調整するウィジェットを生成するクラスです
// 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を渡し、このように使用します
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
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を用意してあげます。
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で提供しています。
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層編は以上になります。
以下の記事も併せてよろしくお願いします!