🐥

Flutterでメモアプリを作る!!

2022/09/27に公開
2

機能要件

  • 友達の基本情報を記録できる。
  • 日記をMarkdownで記録できる。
  • カレンダーから友人の誕生日や日記を確認できる。

システム設計

レイヤー名 機能
app 基本設定, 環境変数
data データベース連携
domain ビジネスロジック・Entity定義
presenter ユーザによる操作イベント処理
provider 状態管理・ビューデータ管理
views 画面表示・ユーザによる操作イベント発信

実装について

クリーンアーキテクチャを参考に設計しました。
クリーンアーキテクチャについて詳しくは以下を参照してください。
https://gist.github.com/mpppk/609d592f25cab9312654b39f1b357c60

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を二つに分けて開発しています。
https://zenn.dev/niccari/articles/22633b3b9d2ef5#6.-view層%3A-状態更新時のデータ取得、画面表示

  • 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),
      ),
    );
  }
}

実装コードです。
https://github.com/daisukemaki1003/man_memo_v2

まとめ

組み込みの現場ではがっつり設計をする機会がなかったのですごく難しかったです。
PresenterとProviderは一緒にしてもいいかなーとか
Presenterは入力と出力を完全に分けたほうがいいのかなーとかとか
改善点は尽きないですね...

思ったことあれば教えていただけると嬉しいですー!

Discussion

subasuba

entityにfreezed使うと事故り辛いですよ

MakiMaki

コメントありがとうございます!!
entityに関しては特にImmutableで実装したほうがいいですよね!
参考にします!