概要
約半年間、独学でFlutterを勉強しました。
自分なりに開発の型が見えてきたのでGitのリポジトリと記事を残したいと思います。
本記事はそのDomain層編です。
flutterプロジェクトの開始時に作成されるサンプルアプリを置き換えたもので、基本的な仕様やUIは一緒ですが、カウントを1つ進めるたびにsharedPreferencesに現在のカウントが保存されるようになっています。
デザインパターン
こちらの大変わかりやすい記事を参考にさせていただきました!
レイヤードアーキテクチャに関しては、こちらの記事内で詳しく(かつ非常にわかりやすく!)説明がされているので、こちらをご参照ください。
ディレクトリ構成
lib
├ features/
│ ├ common_utils/
│ └ #feature-1
│ ├ application/
│ ├ domain/
│ │ ├ entity/
│ │ ├ features/
│ │ └ repository/
│ └ infrastructure/
├ presentation/
└ main.dart
entity
freezedで定義したデータクラスが配置してあります
@freezed
abstract class Count implements _$Count {
const factory Count({
required dynamic updatedAt,
required int value,
}) = _Count;
factory Count.fromJson(Map<String, dynamic> json) => _$CountFromJson(json);
}
features
freezedで定義されたデータクラスの生成、更新、型変換を行うクラスを配置しています。
class CountCreator {
static Count create() {
final DateTime now = DateTime.now();
final Count count = Count(
updatedAt: now,
value: 0,
);
return count;
}
}
このように関数から生成や更新をしてあげることでupdatedAtの値を必ず現在時刻で更新することが保証されます。
型変換はInfrastructure層のrepository内で使用するので、Domain層ではなくInfrastructure層に配置してもいいような気もしますが、entityの型変換を行うという意味でentityに紐づいているという理由と、
@freezed
abstract class Count implements _$Count {
const factory Count({
required dynamic updatedAt,
required int value,
}) = _Count;
factory Count.fromJson(Map<String, dynamic> json) => _$CountFromJson(json);
+ factory Count.fromSharedPreferencesString(String countString) => CountConverter().fromSharedPreferencesString(countString: countString);
}
…としてあげることで
final Count count = Counter.fromSharedPreferencesString(countString: countString)
という書き方で型変換の関数を呼び出すことができることからDomain層に配置しています。
repository
repositoryの抽象クラスとproviderが配置してあります。
final counterRepositoryProvider = Provider<CounterRepository>(
(_) => throw UnimplementedError(),
);
abstract interface class CounterRepository {
//保存したcountを取得する
Future<Count?> fetch();
//countを保存する
Future<void> saveCount(Count count);
}
Mockや本番環境のrepositoryを、このクラスをimplementsして実装してあげることで、メソッドの実装漏れや、引数や返り値の型定義を固定化することができます。
//開発時に利用するモックのrepository
class MockCountRepository implements CounterRepository {
@override
Future<Count?> fetch() async {
return CountCreator.create();
}
@override
Future<void> saveCount(Count count) async {}
}
repositoryの呼び出しはすべてcounterRepositoryProviderを呼び出して実装します。
// saveCountを実行したい!
await _ref.read(counterRepositoryProvider).saveCount(newCount);
counterRepositoryProviderはUnimplementedError()を投げるように実装していますので、本来なら呼び出してもエラーを吐いてしまいます。
そこで、counterRepositoryProviderが提供する値(class)をmain.dartのrunAppのところでoverrideしてあげます。
(詳しくはこちらのrunAppの項目に記載していますので、こちらをご覧ください。)
この実装ではcounterRepositoryProviderの値はPrdCounterRepositoyでoverrideされているので、
// saveCountを実行したい!
await _ref.read(counterRepositoryProvider).saveCount(newCount);
…このsaveCountは
Future<void> saveCount(Count count) async {
final pref = await this.pref;
try {
//countをsharedPreferencesに保存する
//stringに変換してから保存する
final countString =
CountConverter.toSharedPreferencesString(count: count);
//保存
await pref.setString(key, countString);
} catch (e) {
//setIntが失敗した場合は、AppExceptionをthrowする
throw const AppException(countSaveFailed);
}
}
}
この処理が実行されることになります。
Infrastructure層からimplementsしてあげることで、依存の向きをInfrastructure → domainにしているんですね!
おわりに
Domain層編は以上になります。
随時投稿予定の以下の記事もよろしくお願いします!