自分用のFlutterプロジェクトアーキテクチャの整理

2023/09/16に公開

※ 都度更新する

ソースコード

https://github.com/morooka-akira/flutter_architecture

基本的な方針

  • クリーンアーキテクチャをベースにレイヤー分離を行う
  • レイヤー間をまたぐものは依存関係を逆転するためにDIする

ディレクトリ構成

lib/
├── main.dart
├── widget/
│   ├── app.dart
│   └── pages/
│       ├── home_page.dart
│       └── ... (その他のページ)
├── application/
│   └── usecases/
│       ├── get_data_usecase.dart
│       └── ... (その他のユースケース)
├── domain/
│   ├── entities/
│   │   ├── data.dart
│   │   └── ... (その他のエンティティ)
│   └── repositories/
│       ├── data_repository.dart
│       └── ... (その他のリポジトリインターフェース)
├── infrastructure/
│   └── repositories/
│       ├── data_repository_impl.dart
│       └── ... (リポジトリの実装)
└── di/

widget

  • FlutterアプリケーションのView(ウィジェット)をすべてこのディレクトリ配下へ置く
    • app.dart: ルートのアプリコンポーネントを配置

      • テーマの設定やアプリ全体への適応するライブラリの初期化処理はここで実施する。

        • riverpodなど
    • pages/: ページ単位のroot widgetを配置する

      • ページ単位=ルーティングの単位
    • components/: elementsを組み合わせたウィジェット

    • elements/: 単一のウィジェット

    • themas/: テーマ

application

  • アプリケーションのビジネスロジックやユースケースを定義する
    • usecases/: ユースケース

domain

  • ビジネスロジックのルール・モデルを配置する
    • entities/: モデルやエンティティを配置
    • repositories/: データアクセスの抽象化としてのリポジトリインターフェース
      • 実装はinfrastructureにおく

infrastructure

  • アプリケーションの外部のサービスやリソースとのやり取り
    • repositories/: ドメインレイヤーで定義されたリポジトリインターフェースの実装

di

  • 依存性の注入(Dependency Injection)の設定

DIのツール

https://docs-v2.riverpod.dev/

riverpodを使ってDIっぽく書く

(keepAlive: true)
GetAllLanguagesUseCase getAllLanguagesUseCase(GetAllLanguagesUseCaseRef ref) {
  return GetAllLanguagesUseCase(
    repository: ref.read(programingLanguageRepositoryProvider),
  );
}

Widget/pageの構成

  • pageはアプリケーションにおけるページ単位を指す
  • page間は基本ルーティングで遷移
  • pageのアーキテクチャはMVVMを採用してViewModelを介してアクセスする
    • reducerでactionをさばくような構成も考えられるが規模の複雑性との相談。基本はシンプルな構成で作る。

image

ルーティング

ページを切り替えるルーティングは、サードパーティ含めいくつか選択肢があるが、 現在はNavigatorをつかう。

ケースバイケースではあるが、基本の遷移はPathを使い、Pathは一つのファイルにまとめておく。

Navigatorは、navigators配下に配置する。

class PageNavigator extends ConsumerWidget {
  final GlobalKey<NavigatorState> navigatorKey;
  const PageNavigator({required this.navigatorKey, Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Navigator(
      key: navigatorKey,
      initialRoute: PagePath.home,
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case PagePath.home:
            return MaterialPageRoute(
              settings: settings,
              builder: (context) => const HomePage(title: "Home"),
            );
          case PagePath.counter:
            return MaterialPageRoute(
              settings: settings,
              builder: (context) => const CounterPage(
                arguments: CounterPageArguments(title: "Counter"),
              ),
            );
          default:
            return null;
        }
      },
    );
  }
}

UseCase

  • UseCaseは基本1ユースケース1ファイルで定義
    • このあたりは、Andoridが出している Now In Android などの影響を受けている
final class GetAllLanguagesUseCase implements FutureUseCase<void, List<ProgrammingLanguage>> {
  final ProgramingLanguageRepository _repository;
  GetAllLanguagesUseCase({
    required ProgramingLanguageRepository repository,
  }) : _repository = repository;

  
  Future<List<ProgrammingLanguage>> invoke(void input) {
    return _repository.fetchLanguages();
  }
}

Discussion