MVVM+Repositoryパターンを採用したFlutterアプリを構築する
この記事について
wasabeefさんが作成しているリポジトリを参考に、カスタマイズしてテンプレートを作成してみたという自分用メモ的な記事です。導入手順をかなり丁寧に書いたつもりなので、めちゃくちゃ文字数が多くなっちゃっています💦(脅威の50000文字over)。
wasabeefさんのテンプレートでは、ChangeNotifier
を採用していますが、この記事では、StateNotifier
を採用しています。また、Sizer
・Fimber
・Device Preview
なども新たに導入しています。逆に、Dio
の導入はしていません。これは、http
というパッケージを使った方が良いケースもありますし、そもそもFirebase
を使用していて、バックエンドAPIを叩く必要がないケースもあるかと思いますので、必要に応じて、下記のリポジトリを参考にしてみてください。
MVVM+Repositoryパターンとは?
MVVMとは、Model・View・ViewModelで構成されたアーキテクチャのことです。MVVMは、Androidアプリ開発では、定番? のようです。この記事では、MVVMに加えて、Repositoryパターンも採用しています。Repositoryパターンは、ローカルストレージや外部APIのようなデータソースへのアクセスを抽象化するためのデザインパターンです。詳しい解説については、以下の記事を見た方がわかりやすいです。
今回採用している主なパッケージ
- hooks_riverpod
- flutter_hooks
- build_runner
- freezed
- auto_route
- device_preview
Riverpod
Providerで状態を定義し、StatelessWidgetのような状態を持たないウィジェットに対して、状態を注入できるようにしたDIパッケージのことです。StatefullWidgetは、状態とViewが結合しているので、コードが冗長になるという欠点を持っていますが(他にも色々欠点があります)、RiverpodのようなDIパッケージを使用すれば、Viewと状態を分離でき、保守性の向上につながります。
Build Runner
コードの生成とかをコマンド1つでできるようにしたプラグインです。今回は、Freezed
やauto_route
、flutter_gen
などで、コード生成を行うため使用しています。
Freezed
immutableなオブジェクトを生成してくれるプラグインです。また、内部でjson_serializable
を使っているので、オブジェクトとjsonの相互変換も対応しています。
Auto Route
ルーティング周りをいい感じにしてくれるプラグインです。
Device Preview
1つのエミュレータ上で、さまざまな端末のサイズを確認したり、端末の設定(言語・ダークテーマなど)をいじれるようにしたプラグインです。今までは複数のエミュレータを用意して確認していましたが、このプラグインを使えば、エミュレータの切り替えが必要ないので、開発スピードが格段に上がります。
本題
Flutter SDKのバージョンは、stableであるv2.8.1(2021年12月19日現在)を使用します。
Flutterの雛形アプリを作成
$ flutter create flapp
$ cd flapp
$ flutter run
エディタの設定
コードの統一性を高めるために、EditorConfigを設定しておきます。これを設定しておくことで、インデントのサイズ・スタイルや文字コードなど、人それぞれになりそうなものを早めに設定しておくことで、統一感のあるコードを書けるようになります。これは是非とも設定しておきましょう。
$ touch .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
analysis_optionsの設定
Flutter2.5.0から、デフォルトでflutter_lints
が入っており、特に難しい設定いらずで静的解析ができるようになっています。
# コードチェック
$ fvm flutter analyze
# libフォルダ以下をコード整形
$ fvm flutter format lib/
デフォルトの設定のままでも良いのですが、一応自分がいつも使っているものを設定しておきます。
include: package:flutter_lints/flutter.yaml
analyzer:
strong-mode:
implicit-casts: true
implicit-dynamic: true
exclude:
- "**/generated_plugin_registrant.dart"
- "**/build/**"
- "**/generated_*.dart"
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.gr.dart"
- "**/l10n*.dart"
- "**/*.gen.dart"
linter:
rules:
- avoid_types_on_closure_parameters
- avoid_void_async
- await_only_futures
- camel_case_types
- cancel_subscriptions
- close_sinks
- constant_identifier_names
- control_flow_in_finally
- directives_ordering
- empty_statements
- hash_and_equals
- implementation_imports
- non_constant_identifier_names
- package_api_docs
- package_names
- package_prefixed_library_names
- test_types_in_equals
- throw_in_finally
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_new
- unnecessary_statements
- prefer_const_constructors
Prettierの導入
Prettierは、いい感じにコード整形をしてくれるツールです。dartファイルは(多分)Prettierを使えないのですが、今回のプロジェクトには、jsonファイルなどがあり、これらのコードがぐちゃぐちゃなのは個人的には考えられないので、設定しておきます。
$ yarn init -y
$ yarn add -D prettier
$ touch .prettierrc .prettierignore
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}
.dart_tool
.fvm
build
ios
android
node_modulesフォルダが作成されるので、.gitignoreに追加します。
+ node_modules
以下のコマンドで、json・html・yamlファイルなどをコード整形してくれるようになります。
$ yarn prettier --write .
Husky・lint-stagedの導入
Huskyとは、コミット時やプッシュ時などに何らかの処理を実行できるようにするツールです。
lint-stagedとは、ステージングしているファイルに対して何らかの処理を実行するようにできるツールです。
今回はこれらを合わせて、コミット時にコード整形を実装するようにします。
$ yarn add -D husky lint-staged
$ npx husky-init && yarn install
npx husky-initを実行すると、.husky/pre-commitファイルが自動生成されます。
package.jsonにlint-stagedの設定を追加します。下記は、ステージングされているファイルがdartの場合は、fvm flutter formatを実行し、json・yamlの場合は、prettier --writeを実行している例です。
{
+ "lint-staged": {
+ "*.dart": [
+ "fvm flutter format"
+ ],
+ "*.@(json|yaml)": [
+ "prettier --write"
+ ]
+ }
}
.husky/pre-commitを以下のように修正して、コミット時にlint-stagedを実行するようにします。
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
- npm test
+ yarn lint-staged
FVMの設定
FVM(Flutter Version Management)とは、Flutter SDKのバージョンを管理してくれるツールです。実際の開発では、FlutterのSDKのバージョンによっては動かないといったケースが発生しうるので、FVMの設定は必ずといっていいほどやっておいた方が良いです。
fvmコマンドが使えない方は、まず以下のコマンドを実行してください。
$ dart pub global activate fvm
$ fvm --version
2.2.4
fvmの設定ファイルを作成します。fvm_config.jsonでは、flutter SDKのバージョンを指定します。
$ touch .fvm/fvm_config.json .fvm/.gitignore
$ fvm install
$ fvm flutter pub get
{
"flutterSdkVersion": "2.5.3",
"flavors": {}
}
*
!.gitignore
!fvm_config.json
$ fvm releases
..
Oct 15 21 │ 2.5.3
Oct 20 21 │ 2.7.0-3.0.pre
Oct 28 21 │ 2.7.0-3.1.pre
Nov 12 21 │ 2.8.0-3.1.pre
Nov 18 21 │ 2.8.0-3.2.pre
Dec 1 21 │ 2.8.0-3.3.pre
Dec 9 21 │ 2.8.0
--------------------------------------
Dec 15 21 │ 2.9.0-0.1.pre beta
--------------------------------------
--------------------------------------
Dec 15 21 │ 2.9.0-0.1.pre dev
--------------------------------------
--------------------------------------
Dec 16 21 │ 2.8.1 stable
--------------------------------------
main.dartのウィジェットをStatelessWidgetに置き換える
今回、StatefullWidgetを使った状態管理は行わないので、一旦状態を持たないStatelessWidgetに置き換えます。
flutter:
uses-material-design: true
+ assets:
+ - assets/img/
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/img/flutter-icon.png',
width: 200,
),
Text(
'flapp',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
多言語対応(l10n)の導入
多言語対応をするためにintlというパッケージを導入します。
$ fvm flutter pub add intl
dependencies:
flutter:
sdk: flutter
+ flutter_localizations:
+ sdk: flutter
cupertino_icons: ^1.0.2
+ intl: ^0.17.0
genarateフラグをtrueにしておきます。
flutter:
+ generate: true
uses-material-design: true
assets:
- assets/img/
l10nの設定ファイルを追加します。
$ touch l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_ja.arb
output-localization-file: l10n.dart
output-class: L10n
日本語の言語ファイルを追加します。
$ touch lib/l10n/app_ja.arb
{
"@@locale": "ja",
"hello": "こんにちは"
}
以下のコマンドを実行すると、.dart_tool/flutter_gen/gen_l10n/l10n.dart
が作成されます。
$ fvm flutter gen-l10n
main.dartのMaterialAppでlocalizationsDelegates・supportedLocalesを追加します。
import 'package:flutter/material.dart';
+ import 'package:flutter_gen/gen_l10n/l10n.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
+ localizationsDelegates: L10n.localizationsDelegates,
+ supportedLocales: L10n.supportedLocales,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/img/flutter-icon.png',
width: 200,
),
Text(
- 'flapp'
+ L10n.of(context)!.hello,
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
これで、arbファイルに定義した文言が使用できるようになります。
L10n.of(context)!.hello // こんにちは
flutter_genでアセット管理
先ほど、画像を以下のようにして表示していたかと思います。ただパスによる指定だと、typoが怖いので、これを解決するために、flutter_gen
を導入します。
build_runnerと併用して使うので、build_runnerも一緒に導入します。また、svgの対応も一応やっておきます。
$ fvm flutter pub add build_runner -d
$ fvm flutter pub add flutter_gen_runner -d
$ fvm flutter pub add flutter_svg
flutter:
generate: true
uses-material-design: true
assets:
- assets/img/
+ flutter_gen:
+ integrations:
+ flutter_svg: true
以下のコマンドを実行すると、lib/gen/assets.gen.dart
が作成されます。
$ fvm flutter packages pub run build_runner build
main.dartのアセットをlib/gen/assets.gen.dart
のAssetsクラスから呼び出します。
+ import 'package:flapp/gen/assets.gen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Image.asset(
- 'assets/img/flutter-icon.png',
- width: 200,
- ),
+ Assets.img.flutterIcon.image(width: 200),
Text(
L10n.of(context)!.hello,
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
auto_routeの導入
ルーティングの実装には、auto_route
を使用します。
$ mkdir -p lib/ui/xxx
$ touch lib/app.dart lib/ui/xxx/xxx_page.dart
import 'package:flutter/material.dart';
import 'app.dart';
void main() {
runApp(const MyApp());
}
import 'package:flapp/ui/xxx/xxx_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
localizationsDelegates: L10n.localizationsDelegates,
supportedLocales: L10n.supportedLocales,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const XXXPage(),
);
}
}
import 'package:flapp/gen/assets.gen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class XXXPage extends StatelessWidget {
const XXXPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Assets.img.flutterIcon.image(width: 200),
Text(
L10n.of(context)!.hello,
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
}
auto_routeをインストールします。
$ fvm flutter pub add auto_route
$ fvm flutter pub add auto_route_generator -d
app_route.dartとroute_path.dartを作成します。
$ mkdir -p lib/ui/routes
$ touch lib/ui/routes/app_route.dart lib/ui/routes/route_path.dart
import 'package:auto_route/auto_route.dart';
import 'package:flapp/ui/routes/route_path.dart';
import 'package:flapp/ui/xxx/xxx_page.dart';
(
replaceInRouteName: 'Page,Route',
routes: [
AutoRoute(
path: RoutePath.appRouteXXX,
page: XXXPage,
initial: true,
),
],
)
class $AppRouter {}
class RoutePath {
static const appRouteXXX = '/xxx';
}
lib/app.dartを修正します。
- import 'package:flapp/ui/xxx/xxx_page.dart';
+ import 'package:flapp/ui/routes/app_route.gr.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
+ final appRouter = AppRouter();
- return MaterialApp(
- title: 'Flutter Demo',
- localizationsDelegates: L10n.localizationsDelegates,
- supportedLocales: L10n.supportedLocales,
- theme: ThemeData(
- primarySwatch: Colors.blue,
- ),
- home: const XXXPage(),
- );
+ return MaterialApp.router(
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ localizationsDelegates: L10n.localizationsDelegates,
+ supportedLocales: L10n.supportedLocales,
+ routeInformationParser: appRouter.defaultRouteParser(),
+ routerDelegate: appRouter.delegate(),
+ );
}
}
Riverpod・Flutter Hooksの導入
$ fvm flutter pub add hooks_riverpod
$ fvm flutter pub add flutter_hooks
main.dartのMyApp()
をProviderScope
でラップします。
import 'package:flutter/material.dart';
+ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
void main() {
- runApp(const MyApp());
+ runApp(const ProviderScope(child: MyApp()));
}
Freezedの導入
$ fvm flutter pub add freezed -d
$ fvm flutter pub add json_serializable
$ fvm flutter pub add freezed_annotation
試しにxxxページの状態をfreezedを用いて定義したいと思います。
$ touch lib/ui/xxx/xxx_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'xxx_state.freezed.dart';
class XXXState with _$XXXState {
const factory XXXState({
(0) int count,
}) = _XXXState;
}
freezedもbuild_runnerを実行してあげることで、xxx_state.freezed.dart
が自動生成されます。
$ fvm flutter packages pub run build_runner build
ViewModelの実装
今回ViewModelは、StateNotifier
で実装します。StateNotifier
はシングルソースの状態を持ちます。
$ touch lib/ui/xxx/xxx_view_model.dart
以下がViewModelの実装コードです。今後、非同期の値を扱うことを見据えて、シングルソースの状態は、先ほど作成したXXXState
ではなく、AsyncValue<XXXState>
としています。
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'xxx_state.dart';
final xxxViewModelProvider =
StateNotifierProvider.autoDispose<XXXViewModel, AsyncValue<XXXState>>(
(ref) => XXXViewModel(ref: ref),
);
class XXXViewModel extends StateNotifier<AsyncValue<XXXState>> {
final AutoDisposeStateNotifierProviderRef _ref;
XXXViewModel({required AutoDisposeStateNotifierProviderRef ref})
: _ref = ref,
super(const AsyncLoading()) {
load();
}
// 初期化
void load() {
state = const AsyncValue.data(
XXXState(count: 0),
);
}
// カウントを+1
void increment() {
final count = state.value!.count;
state = AsyncValue.data(
XXXState(count: count + 1),
);
}
}
先ほど作成したxxx_page.dartを修正します。
import 'package:flapp/gen/assets.gen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'xxx_view_model.dart';
class XXXPage extends HookConsumerWidget {
const XXXPage({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(xxxViewModelProvider);
final viewModel = ref.watch(xxxViewModelProvider.notifier);
return state.when(
data: (data) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Assets.img.flutterIcon.image(width: 200),
Text(
L10n.of(context)!.hello,
style: Theme.of(context).textTheme.headline4,
),
ElevatedButton(
onPressed: viewModel.increment,
child: Text(data.count.toString()),
),
],
),
),
),
);
},
error: (e, msg) => Text(e.toString()),
loading: () {
return const Scaffold(
body: SafeArea(
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
);
}
}
XXXPageはStatelessWidget
で実装していましたが、ViewModelを注入(DI)するためにHookConsumerWidget
で置き換えています。
シングルソースの状態へのアクセスは以下のコードになります。
final state = ref.watch(xxxViewModelProvider);
ViewModelのインスタンス自体へのアクセスは以下のコードになります。
final viewModel = ref.watch(xxxViewModelProvider.notifier);
また、状態はAsyncValue
なので、そのままデータを取り出すことができません。なので、whenメソッドを使って、正常時・エラー時・ローディング時のそれぞれの場合で実装してあげる必要があります。
state.when(
data: (data) {
// 正常時
},
error: (e, msg) {
// エラー時
},
loading: () {
// ローディング時
}
)
ボタンをクリックすると、カウントアップしてくれるようになりました。
Repositoryの実装
Repositoryは今後テストを書くことを考えて、抽象クラスとImplementクラスで構成します。
Repositoryでは、非同期の値を扱うため、正常時・エラー時のハンドリングがしやすいように、Resultクラスを作成します。
$ mkdir -p lib/data/model/result
$ touch lib/data/model/result/result.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'result.freezed.dart';
abstract class Result<T> with _$Result<T> {
const Result._();
const factory Result.success({required T data}) = Success<T>;
const factory Result.failure({required Exception error}) = Failure<T>;
static Result<T> guard<T>(T Function() body) {
try {
return Result.success(data: body());
} on Exception catch (e) {
return Result.failure(error: e);
}
}
static Future<Result<T>> guardFuture<T>(Future<T> Function() future) async {
try {
return Result.success(data: await future());
} on Exception catch (e) {
return Result.failure(error: e);
}
}
bool get isSuccess => when(success: (data) => true, failure: (e) => false);
bool get isFailure => !isSuccess;
T get dataOrThrow {
return when(
success: (data) => data,
failure: (e) => throw e,
);
}
}
extension ResultObjectExt<T> on T {
Result<T> get asSuccess => Result.success(data: this);
Result<T> asFailure(Exception e) => Result.failure(error: e);
}
fetch関数を呼び出すと、2秒後にランダムな数字を返すxxxRepository
を実装します。
$ mkdir -p lib/data/repository/xxx
$ touch lib/data/repository/xxx/xxx_repository.dart
$ touch lib/data/repository/xxx/xxx_repository_impl.dart
抽象クラス
import 'package:flapp/data/model/result/result.dart';
abstract class XXXRepository {
Future<Result<int>> fetch();
}
実装クラス(Implementクラス)
import 'dart:math';
import 'package:flapp/data/model/result/result.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'xxx_repository.dart';
final xxxRepositoryProvider =
Provider<XXXRepository>((ref) => XXXRepositoryImpl(ref.read));
class XXXRepositoryImpl implements XXXRepository {
XXXRepositoryImpl(this._reader);
final Reader _reader;
Future<Result<int>> fetch() async {
return Result.guardFuture(() async {
await Future.delayed(const Duration(seconds: 2)); // 2秒待機
final rand = Random();
return rand.nextInt(100); // 0~100の乱数
});
}
}
ViewModel側で、xxxRepositoryを呼び出します。
+ import 'package:flapp/data/repository/xxx/xxx_repository.dart';
+ import 'package:flapp/data/repository/xxx/xxx_repository_impl.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'xxx_state.dart';
final xxxViewModelProvider =
StateNotifierProvider.autoDispose<XXXViewModel, AsyncValue<XXXState>>(
(ref) => XXXViewModel(ref: ref),
);
class XXXViewModel extends StateNotifier<AsyncValue<XXXState>> {
final AutoDisposeStateNotifierProviderRef _ref;
XXXViewModel({required AutoDisposeStateNotifierProviderRef ref})
: _ref = ref,
super(const AsyncLoading()) {
load();
}
+ // xxxRepository
+ late final XXXRepository xxxRepository = _ref.read(xxxRepositoryProvider);
// 初期化
- void load() {
- state = const AsyncValue.data(
- XXXState(count: 0),
- );
- }
+ Future<void> load() async {
+ final result = await xxxRepository.fetch();
+ result.when(
+ success: (data) {
+ state = AsyncValue.data(
+ XXXState(count: data),
+ );
+ },
+ failure: (error) {
+ state = AsyncValue.error(error);
+ },
+ );
+ }
// カウントを+1
void increment() {
final count = state.value!.count;
state = AsyncValue.data(
XXXState(count: count + 1),
);
}
}
2秒間ロード画面が表示された後に、ランダムな数がセットされるようになります。
カスタムテーマの実装
色やフォントなどを詳細に設定していきます。
カラーテーマは、app_colors.dartで定義していきます。
$ mkdir -p lib/ui/theme
$ touch lib/ui/theme/app_colors.dart
import 'package:flutter/material.dart';
class AppColors {
const AppColors({
required this.primary,
required this.primaryVariant,
required this.onPrimary,
required this.secondary,
required this.secondaryVariant,
required this.onSecondary,
required this.background,
required this.onBackground,
required this.surface,
required this.onSurface,
required this.error,
required this.onError,
});
factory AppColors.light() {
return const AppColors(
primary: Color(0xFF6200EE),
primaryVariant: Color(0xFF3700B3),
onPrimary: Color(0xFFFFFFFF),
secondary: Color(0xFF03DAC6),
secondaryVariant: Color(0xFF018786),
onSecondary: Color(0xFF000000),
background: Color(0xFFFFFFFF),
onBackground: Color(0xFF000000),
surface: Color(0xFFFFFFFF),
onSurface: Color(0xFF000000),
error: Color(0xFFB00020),
onError: Color(0xFFFFFFFF),
);
}
factory AppColors.dark() {
return const AppColors(
primary: Color(0xFF6200EE),
primaryVariant: Color(0xFF3700B3),
onPrimary: Color(0xFFFFFFFF),
secondary: Color(0xFF03DAC6),
secondaryVariant: Color(0xFF018786),
onSecondary: Color(0xFF000000),
background: Color(0xFFFFFFFF),
onBackground: Color(0xFF000000),
surface: Color(0xFFFFFFFF),
onSurface: Color(0xFF000000),
error: Color(0xFFB00020),
onError: Color(0xFFFFFFFF),
);
}
/// Material Colors https://material.io/design/color/the-color-system.html#color-theme-creation
final Color primary;
final Color primaryVariant;
final Color onPrimary;
final Color secondary;
final Color secondaryVariant;
final Color onSecondary;
final Color background;
final Color onBackground;
final Color surface;
final Color onSurface;
final Color error;
final Color onError;
}
テキストテーマは、app_text_theme.dart
で定義していきます。
$ touch lib/ui/theme/app_text_theme.dart
$ touch lib/ui/theme/font_size.dart
class FontSize {
/// 60pt
static const double pt60 = 60;
/// 48pt
static const double pt48 = 48;
/// 40pt
static const double pt40 = 40;
/// 32pt
static const double pt32 = 32;
/// 28pt
static const double pt28 = 28;
/// 24pt
static const double pt24 = 24;
/// 20pt
static const double pt20 = 20;
/// 18pt
static const double pt18 = 18;
/// 16pt
static const double pt16 = 16;
/// 14pt
static const double pt14 = 14;
/// 12pt
static const double pt12 = 12;
/// 10pt
static const double pt10 = 10;
/// 8pt
static const double pt8 = 8;
}
フォントファミリーの指定は、GoogleFontを使用するので、インストールしていきます。フォントは「NotoSans」を指定しています。
$ fvm flutter pub add google_fonts
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'font_size.dart';
class AppTextTheme {
const AppTextTheme._({
required this.h10,
required this.h20,
required this.h30,
required this.h40,
required this.h50,
required this.h60,
required this.h70,
required this.h80,
required this.h90,
required this.h100,
});
factory AppTextTheme() {
final _normalRegular = GoogleFonts.notoSans(
textStyle: const TextStyle(
fontWeight: FontWeight.w400,
height: 1.5,
leadingDistribution: TextLeadingDistribution.even,
),
);
return AppTextTheme._(
h10: const TextStyle(fontSize: FontSize.pt10).merge(_normalRegular),
h20: const TextStyle(fontSize: FontSize.pt12).merge(_normalRegular),
h30: const TextStyle(fontSize: FontSize.pt14).merge(_normalRegular),
h40: const TextStyle(fontSize: FontSize.pt16).merge(_normalRegular),
h50: const TextStyle(fontSize: FontSize.pt20).merge(_normalRegular),
h60: const TextStyle(fontSize: FontSize.pt24).merge(_normalRegular),
h70: const TextStyle(fontSize: FontSize.pt32).merge(_normalRegular),
h80: const TextStyle(fontSize: FontSize.pt40).merge(_normalRegular),
h90: const TextStyle(fontSize: FontSize.pt48).merge(_normalRegular),
h100: const TextStyle(fontSize: FontSize.pt60).merge(_normalRegular),
);
}
/// pt10
final TextStyle h10;
/// pt12
final TextStyle h20;
/// pt14
final TextStyle h30;
/// pt16
final TextStyle h40;
/// pt20
final TextStyle h50;
/// pt24
final TextStyle h60;
/// pt32
final TextStyle h70;
/// pt40
final TextStyle h80;
/// pt48
final TextStyle h90;
/// pt60
final TextStyle h100;
}
extension TextStyleExt on TextStyle {
TextStyle bold() => copyWith(fontWeight: FontWeight.w700);
TextStyle comfort() => copyWith(height: 1.8);
TextStyle dense() => copyWith(height: 1.2);
}
カラーテーマとテキストテーマを1つにまとめたAppThemeクラスを実装します。
$ touch lib/ui/theme/app_theme.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app_colors.dart';
import 'app_text_theme.dart';
final appThemeModeProvider =
StateNotifierProvider<StateController<ThemeMode>, ThemeMode>(
(ref) => StateController(ThemeMode.light),
);
final appThemeProvider = Provider<AppTheme>(
(ref) {
final mode = ref.watch(appThemeModeProvider);
switch (mode) {
case ThemeMode.dark:
return AppTheme.dark();
case ThemeMode.light:
default:
return AppTheme.light();
}
},
);
class AppTheme {
AppTheme({
required this.mode,
required this.data,
required this.textTheme,
required this.appColors,
});
factory AppTheme.light() {
const mode = ThemeMode.light;
final appColors = AppColors.light();
final themeData = ThemeData.light().copyWith(
scaffoldBackgroundColor: appColors.background,
textTheme: GoogleFonts.notoSansTextTheme(ThemeData.light().textTheme),
snackBarTheme: SnackBarThemeData(
backgroundColor: appColors.error,
behavior: SnackBarBehavior.floating,
),
);
return AppTheme(
mode: mode,
data: themeData,
textTheme: AppTextTheme(),
appColors: appColors,
);
}
factory AppTheme.dark() {
const mode = ThemeMode.dark;
final appColors = AppColors.dark();
final themeData = ThemeData.dark().copyWith(
scaffoldBackgroundColor: appColors.background,
textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme),
snackBarTheme: SnackBarThemeData(
backgroundColor: appColors.error,
behavior: SnackBarBehavior.floating,
),
);
return AppTheme(
mode: mode,
data: themeData,
textTheme: AppTextTheme(),
appColors: appColors,
);
}
final ThemeMode mode;
final ThemeData data;
final AppTextTheme textTheme;
final AppColors appColors;
}
最後に、先ほど作成したカスタムテーマをapp.dartに適用していきます。カスタムテーマはProviderで定義しているので、app.dartのStatelessWidget
をHookConsumerWidget
に変更します。
import 'package:flapp/ui/routes/app_route.gr.dart';
+ import 'package:flapp/ui/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
+ import 'package:flutter_hooks/flutter_hooks.dart';
+ import 'package:hooks_riverpod/hooks_riverpod.dart';
- class MyApp extends StatelessWidget {
+ class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = ref.watch(appThemeProvider);
+ final themeMode = ref.watch(appThemeModeProvider);
- final appRouter = AppRouter();
+ final appRouter = useMemoized(() => AppRouter());
return MaterialApp.router(
debugShowCheckedModeBanner: false,
+ theme: theme.data,
+ darkTheme: AppTheme.dark().data,
+ themeMode: themeMode,
localizationsDelegates: L10n.localizationsDelegates,
supportedLocales: L10n.supportedLocales,
routeInformationParser: appRouter.defaultRouteParser(),
routerDelegate: appRouter.delegate(),
);
}
}
AppThemeを使って、xxxページのスタイルを整えていきます。
import 'package:flapp/gen/assets.gen.dart';
+ import 'package:flapp/ui/theme/app_text_theme.dart';
+ import 'package:flapp/ui/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'xxx_view_model.dart';
class XXXPage extends HookConsumerWidget {
const XXXPage({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
+ final theme = ref.watch(appThemeProvider);
+ final state = ref.watch(xxxViewModelProvider);
final viewModel = ref.watch(xxxViewModelProvider.notifier);
return state.when(
data: (data) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Assets.img.flutterIcon.image(width: 200),
Text(
L10n.of(context)!.hello,
+ style: theme.textTheme.h70.bold(),
),
ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ primary: theme.appColors.primary,
+ onPrimary: theme.appColors.onPrimary,
+ ),
onPressed: viewModel.increment,
child: Text(data.count.toString()),
),
],
),
),
),
);
},
error: (e, msg) => Text(e.toString()),
loading: () {
return Scaffold(
body: SafeArea(
child: Center(
child: CircularProgressIndicator(
+ color: theme.appColors.primary,
),
),
),
);
},
);
}
}
テキストのスタイルは以下のように指定しています。
Text(
L10n.of(context)!.hello,
style: theme.textTheme.h70.bold(),
),
色とかを変更したい場合は、copyWith
を使います。
Text(
L10n.of(context)!.hello,
style: theme.textTheme.h70
.bold()
.copyWith(color: theme.appColors.onSecondary),
),
カスタムフックの実装
l10nを使用するとき、こんな感じでcontextを渡していたと思います。
L10n.of(context)!.hello // こんにちは
flutter_hooksには、useContext
というhook関数が用意されています。今回はuseContext
を利用して、l10nにアクセスするhook関数を実装したいと思います。
$ mkdir -p lib/ui/hooks
$ touch lib/ui/hooks/use_l10n.dart
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
L10n useL10n() {
final context = useContext();
return L10n.of(context)!;
}
呼び出し側は、こんな感じでcontextを渡すことなく、l10nにアクセスできます。
final l10n = useL10n();
l10n.hello // こんにちは
import 'package:flapp/gen/assets.gen.dart';
+ import 'package:flapp/ui/hooks/use_l10n.dart';
import 'package:flapp/ui/theme/app_text_theme.dart';
import 'package:flapp/ui/theme/app_theme.dart';
import 'package:flutter/material.dart';
- import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'xxx_view_model.dart';
class XXXPage extends HookConsumerWidget {
const XXXPage({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(appThemeProvider);
final state = ref.watch(xxxViewModelProvider);
final viewModel = ref.watch(xxxViewModelProvider.notifier);
+ final l10n = useL10n();
return state.when(
data: (data) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Assets.img.flutterIcon.image(width: 200),
Text(
- L10n.of(context)!.hello,
+ l10n.hello,
style: theme.textTheme.h70.bold(),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: theme.appColors.primary,
onPrimary: theme.appColors.onPrimary,
),
onPressed: viewModel.increment,
child: Text(data.count.toString()),
),
],
),
),
),
);
},
// 省略
);
}
}
同様に、RouterとMediaQuery用のhook関数を実装しておきます。
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
StackRouter useRouter() {
final context = useContext();
return AutoRouter.of(context);
}
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
MediaQueryData useMediaQuery() {
final context = useContext();
return MediaQuery.of(context);
}
Sizerの導入
Flutterで作成したアプリは、スマートフォン・タブレット・PCなど、さまざまな端末で使われる可能性があるので、レスポンシブデザインに対応させる必要があります。そこで、Sizer
というプラグインを使用すれば、簡単にレスポンシブ化させることができます。詳しい使い方については、以下をご参照ください。
$ fvm flutter pub add sizer
import 'package:flapp/ui/routes/app_route.gr.dart';
import 'package:flapp/ui/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+ import 'package:sizer/sizer.dart';
class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(appThemeProvider);
final themeMode = ref.watch(appThemeModeProvider);
final appRouter = useMemoized(() => AppRouter());
- return MaterialApp.router(
- debugShowCheckedModeBanner: false,
- theme: theme.data,
- darkTheme: AppTheme.dark().data,
- themeMode: themeMode,
- localizationsDelegates: L10n.localizationsDelegates,
- supportedLocales: L10n.supportedLocales,
- routeInformationParser: appRouter.defaultRouteParser(),
- routerDelegate: appRouter.delegate(),
- );
+ return Sizer(
+ builder: (context, orientation, deviceType) => MaterialApp.router(
+ debugShowCheckedModeBanner: false,
+ theme: theme.data,
+ darkTheme: AppTheme.dark().data,
+ themeMode: themeMode,
+ localizationsDelegates: L10n.localizationsDelegates,
+ supportedLocales: L10n.supportedLocales,
+ routeInformationParser: appRouter.defaultRouteParser(),
+ routerDelegate: appRouter.delegate(),
+ ),
+ );
}
}
Fimberの導入
Fimberは、ロギング用のライブラリになります。リリース環境では、ログを垂れ流しにするのは、よくないので、Fimberの設定で、デバッグ環境でのみログを流すようにします。
$ fvm flutter pub add fimber -d
+ import 'package:fimber/fimber.dart';
+ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
void main() {
+ // Fimber
+ if (!kReleaseMode) {
+ Fimber.plantTree(DebugTree());
+ } else {
+ debugPrint = (message, {wrapWidth}) {};
+ }
runApp(const ProviderScope(child: MyApp()));
}
また、runZonedGuarded
でアプリ内で発生したエラーを捕捉するようにします。本当は、Firebase Crashlytics
とかを使って、クラッシュレポートを送った方が良いですが、今回はFimberでログを出力するようにしておきます。
+ import 'dart:async';
import 'package:fimber/fimber.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
void main() {
// Fimber
if (!kReleaseMode) {
Fimber.plantTree(DebugTree());
} else {
debugPrint = (message, {wrapWidth}) {};
}
- runApp(const ProviderScope(child: MyApp()));
+ runZonedGuarded(
+ () => runApp(const ProviderScope(child: MyApp())),
+ (error, stackTrace) {
+ Fimber.e(error.toString());
+ },
+ );
}
環境変数の実装
Flutter 1.17から、環境変数を--dart-define
というオプションで渡せるようになりました。今回は、FLAVOR
という環境変数を渡すようにし、開発環境(dev)と本番環境(prod)でアプリ内の環境変数を切り替えできるようにしたいと思います。
また、enumとStringの相互変換を行うため、enum_to_string
を導入しています。
$ fvm flutter pub add enum_to_string
$ mkdir -p lib/foundation
$ touch lib/foundation/constants.dart
import 'package:enum_to_string/enum_to_string.dart';
enum Flavor { dev, prod }
class Constants {
const Constants._({required this.baseUrl});
factory Constants.of() {
switch (flavor) {
case Flavor.dev:
return Constants._dev();
case Flavor.prod:
default:
return Constants._prod();
}
}
factory Constants._dev() {
return const Constants._(baseUrl: 'http://dev');
}
factory Constants._prod() {
return const Constants._(baseUrl: 'http://prod');
}
/// エンドポイント
final String baseUrl;
/// 環境 Flavor
static Flavor get flavor =>
EnumToString.fromString(
Flavor.values,
const String.fromEnvironment('FLAVOR'),
) ??
Flavor.dev;
}
起動オプションに--dart-define=FLAVOR=
を渡すだけで、環境変数の切り替えができるようになりました。指定しない場合は、dev環境になります。
# 開発環境(dev)
$ fvm flutter run --dart-define=FLAVOR=dev --target lib/main.dart
# 本番環境(prod)
$ fvm flutter run --dart-define=FLAVOR=prod --target lib/main.dart
開発環境の時は、わかりやすいようにtoastを表示したいと思います。toast表示は、fluttertoast
を導入します。
$ fvm flutter pub add fluttertoast
import 'dart:async';
+ import 'package:enum_to_string/enum_to_string.dart';
import 'package:fimber/fimber.dart';
+ import 'package:flapp/foundation/constants.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
- void main() {
+ Future<void> main() async {
+ WidgetsFlutterBinding.ensureInitialized();
// Fimber
if (!kReleaseMode) {
Fimber.plantTree(DebugTree());
} else {
debugPrint = (message, {wrapWidth}) {};
}
+ // Display Toast (Only Dev)
+ if (Constants.flavor == Flavor.dev) {
+ Fluttertoast.showToast(
+ msg: "flavor: ${EnumToString.convertToString(Constants.flavor)}",
+ );
+ }
runZonedGuarded(
() => runApp(const ProviderScope(child: MyApp())),
(error, stackTrace) {
Fimber.e(error.toString());
},
);
}
Device Previewの導入
Device Preview
は、複数のデバイスの画面サイズを1つのエミュレータで確認できるツールです。デバイスの切り替えだけでなく、言語やダークモードなどの設定ができるので、非常に素晴らしいツールです。
$ fvm flutter pub add device_preview
Device Previewの設定をします。リリースモード時は、Device Previewを無効にします。
import 'dart:async';
+ import 'package:device_preview/device_preview.dart';
import 'package:enum_to_string/enum_to_string.dart';
import 'package:fimber/fimber.dart';
import 'package:flapp/foundation/constants.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
Future<void> main() async {
// 省略
runZonedGuarded(
- () => runApp(const ProviderScope(child: MyApp())),
+ () => runApp(
+ ProviderScope(
+ child: DevicePreview(
+ enabled: !kReleaseMode,
+ builder: (context) {
+ return const MyApp();
+ },
+ ),
+ ),
+ ),
(error, stackTrace) {
Fimber.e(error.toString());
},
);
}
言語の設定(locale)やMediaQueryをDevicePreviewから引き継ぐ設定もします。
+ import 'package:device_preview/device_preview.dart';
import 'package:flapp/ui/routes/app_route.gr.dart';
import 'package:flapp/ui/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(appThemeProvider);
final themeMode = ref.watch(appThemeModeProvider);
final appRouter = useMemoized(() => AppRouter());
return Sizer(
builder: (context, orientation, deviceType) => MaterialApp.router(
debugShowCheckedModeBanner: false,
+ useInheritedMediaQuery: true,
theme: theme.data,
darkTheme: AppTheme.dark().data,
themeMode: themeMode,
+ locale: DevicePreview.locale(context),
localizationsDelegates: L10n.localizationsDelegates,
supportedLocales: L10n.supportedLocales,
routeInformationParser: appRouter.defaultRouteParser(),
routerDelegate: appRouter.delegate(),
),
);
}
}
先ほど使用した、--dart-define
でDevice Previewの有効にするかどうかの制御ができるようにします。
import 'package:enum_to_string/enum_to_string.dart';
enum Flavor { dev, prod }
class Constants {
// 省略
+ /// Device Previewを有効化するかどうか
+ static bool get enablePreview => const bool.fromEnvironment('PREVIEW');
}
import 'dart:async';
import 'package:device_preview/device_preview.dart';
import 'package:enum_to_string/enum_to_string.dart';
import 'package:fimber/fimber.dart';
import 'package:flapp/foundation/constants.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
Future<void> main() async {
// 省略
runZonedGuarded(
() => runApp(
ProviderScope(
child: DevicePreview(
- enabled: !kReleaseMode,
+ enabled: !kReleaseMode && Constants.enablePreview,
builder: (context) {
return const MyApp();
},
),
),
),
(error, stackTrace) {
Fimber.e(error.toString());
},
);
}
--dart-define=PREVIEW=true
を渡すと、Device Previewが有効化になります。
$ fvm flutter run --dart-define=FLAVOR=dev --dart-define=PREVIEW=true --target lib/main.dart
Makefileでよく使うショットカットコマンドを実装
Makefileを作成すると、shellコマンドをmakeコマンドという形で自分で作成できるようになります。例えば、fvm flutter format lib
をmake format
で実行できるようにできます。
$ touch Makefile
下記は、自分がFlutterをやるときにいつも使っているMakefileになります。
################################################################################################
## 環境変数 flavor
################################################################################################
DEV_FLAVOR := dev
PROD_FLAVOR := prod
################################################################################################
## 基本コマンド
################################################################################################
# setup
.PHONY: setup
setup:
dart pub global activate fvm
fvm install
yarn install
# packages install
.PHONY: dependencies
dependencies:
fvm flutter pub get
# packages upgrade
.PHONY: upgrade
upgrade:
fvm flutter packages upgrade
# packages clean
.PHONY: clean
clean:
fvm flutter clean
# code format
.PHONY: format
format:
fvm flutter format lib/
# code analyze
.PHONY: analyze
analyze:
fvm flutter analyze
# l10n generate
.PHONY: l10n
l10n:
fvm flutter gen-l10n
# code generate
.PHONY: build-runner
build-runner:
fvm flutter packages pub run build_runner build --delete-conflicting-outputs
# code generate watch
.PHONY: build-runner-watch
build-runner-watch:
fvm flutter packages pub run build_runner watch --delete-conflicting-outputs
# run test
.PHONY: test
test:
fvm flutter test
################################################################################################
## 実行・ビルド
################################################################################################
# run dev
.PHONY: run-dev
run-dev:
fvm flutter run --dart-define=FLAVOR=${DEV_FLAVOR} --target lib/main.dart
# run dev (with Device Preview)
.PHONY: run-dev-preview
run-dev-preview:
fvm flutter run --dart-define=FLAVOR=${DEV_FLAVOR} --dart-define=PREVIEW=true --target lib/main.dart
# run prod
.PHONY: run-prod
run-prod:
fvm flutter run --release --dart-define=FLAVOR=${PROD_FLAVOR} --target lib/main.dart
# build APK dev
.PHONY: build-android-dev
build-android-dev:
fvm flutter build apk --dart-define=FLAVOR=$(DEV_FLAVOR) --target lib/main.dart
# build APK prod
.PHONY: build-android-prod
build-android-prod:
fvm flutter build apk --release --dart-define=FLAVOR=$(PROD_FLAVOR) --target lib/main.dart
# build IOS dev
.PHONY: build-ios-dev
build-ios-dev:
fvm flutter build ios --dart-define=FLAVOR=$(DEV_FLAVOR) --target lib/main.dart
# build IOS prod
.PHONY: build-ios-prod
build-ios-prod:
fvm flutter build ios --release --dart-define=FLAVOR=$(PROD_FLAVOR) --target lib/main.dart
以下のコマンドなどが使えるようになります。
# format
make format
# 実行(dev環境)
make run-dev
# 実行(dev環境・Device Preview有効)
make run-dev-preview
Visual Studio Codeの設定
vscodeでデバッグ構成をいくつか追加しておきます。
$ mkdir -p .vscode
$ touch .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "run dev",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["--dart-define=FLAVOR=dev"]
},
{
"name": "run dev (with Device Preview)",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["--dart-define=FLAVOR=dev", "--dart-define=PREVIEW=true"]
},
{
"name": "run prod",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["--dart-define=FLAVOR=prod"]
}
]
}
ファイル非表示の設定もできます。node_modules
とかは基本中を見ることはないと思うので、設定しておきます。
$ touch .vscode/settings.json
{
"files.exclude": {
".dart_tool": true,
".idea": true,
"build": true,
"node_modules": true,
".flutter-plugins": true,
".flutter-plugins-dependencies": true,
".metadata": true,
".packages": true,
"yarn.lock": true,
"pubspec.lock": true
}
}
終わりに
今回のプロジェクト構成は、同じく学生エンジニアの友達の影響をもろに受けてます😋
備忘録として書いたつもりが、めちゃくちゃ丁寧にまとめてしまって、気づいたら文字数がエグいことになってました笑(卒論はこれで提出したい、、)
ちなみにリポジトリはこちらに公開しておりますので、テンプレートとして使ってもらっても全然OKです!
Discussion
大変参考になりました!ありがとうございます!
一点質問なのですが、カメラ撮影時などに、画像がデバイス領域に保存される処理に関してはrepositoryに記載すべきでしょうか?
それともライブラリで簡略化されてて、行数が少ないのでそのままmodelやviewModelに記載すべきでしょうか?
良ければアドバイスいただきたいです。