🐥
Flutterでメモアプリを作る!!
機能要件
- 友達の基本情報を記録できる。
- 日記をMarkdownで記録できる。
- カレンダーから友人の誕生日や日記を確認できる。
システム設計
レイヤー名 | 機能 |
---|---|
app | 基本設定, 環境変数 |
data | データベース連携 |
domain | ビジネスロジック・Entity定義 |
presenter | ユーザによる操作イベント処理 |
provider | 状態管理・ビューデータ管理 |
views | 画面表示・ユーザによる操作イベント発信 |
実装について
クリーンアーキテクチャを参考に設計しました。
クリーンアーキテクチャについて詳しくは以下を参照してください。
1. Domain
ディレクトリ名 | 機能 |
---|---|
entity | Entity定義 |
i_repositories | データ層操作(repositories)のインターフェース |
interactor | ユースケースの実装部分 |
usecases | ユースケース(ビジネスロジック)のインターフェース |
Userデータは以下のように定義。
class User {
final String userId;
final DateTime createdAt;
final String name;
String? iconPath;
...
}
class UserDetail {
final User user;
final int age; // 年齢
final String birthday; // 誕生日
final String birthplace; // 出身地
final String residence; // 居住地
final int holiday; // 休日
final String occupation; // 職業
final String memo; // メモ
...
}
UseCaseではビジネスルールのインターフェースを定義しています。
同階層でInputData・OutputDataのインターフェースも定義しています。
PresenterはUseCaseを介してドメイン層にアクセスします。
abstract class UseCase<InputData, OutputData> {
OutputData handle(InputData input);
}
2. Presenter
Views層におけるdomain操作処理を実装しています。
class UserGetListPresenter {
Transformer transformer = Transformer();
final UserGetListUseCase _usecase = UserGetListInteractor();
Future<List<UserModel>> handle() async {
final input = UserGetListInput();
final output = _usecase.handle(input);
...
return output;
}
3. Provider
ProviderではUIを制御しているViewStateを操作の操作と、
Presenterを介してユーザーからのイベント処理をドメイン層に伝えます。
本アプリではUIから受け取ったイベントを操作するプロバイダーと
Stateを管理するプロバイダーに分割しています。
/// Event
class UserActions {
UserActions(this.ref);
...
}
/// State
class UserListNotifier extends StateNotifier<AsyncValue<UserViewModel>> {
UserListNotifier(this.ref) : super(const AsyncValue<UserViewModel>.loading());
...
}
4. Views
今回は以下ブログを参考にWidgetを二つに分けて開発しています。
- Component: Containerから受け取ったデータを使ってUIを構築します。Containerを介してデータにアクセスするため状態に依存しません。最も低いレイヤーに位置します。
- Container: Providerから受け取ったデータをComponentに提供します。ConsumerWidgetで定義。
class UserAddButtonContainer extends ConsumerWidget {
const UserAddButtonContainer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final userActions = ref.watch(userActionsProvider);
add() async {
await userActions.add("test");
}
return UserAddButtonComponent(add);
}
}
class UserAddButtonComponent extends StatelessWidget {
const UserAddButtonComponent(this.addUser, {super.key});
final Function() addUser;
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
await addUser();
},
child: Container(
height: 54,
width: 54,
margin: const EdgeInsets.only(right: 17, bottom: 50),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50),
),
child: const Icon(Icons.add, size: 25, color: Colors.white),
),
);
}
}
実装コードです。
まとめ
組み込みの現場ではがっつり設計をする機会がなかったのですごく難しかったです。
PresenterとProviderは一緒にしてもいいかなーとか
Presenterは入力と出力を完全に分けたほうがいいのかなーとかとか
改善点は尽きないですね...
思ったことあれば教えていただけると嬉しいですー!
Discussion
entityにfreezed使うと事故り辛いですよ
コメントありがとうございます!!
entityに関しては特にImmutableで実装したほうがいいですよね!
参考にします!