Flutterアプリのアーキテクチャについて


会社で開発しているヘルスケアアプリの一部の機能をFlutterで実装するにあたって、Flutterアプリ実装におけるアーキテクチャやフォルダの構成について検討したのでまとめる。

前提

  • Providerを使用
  • 最近のAndroidアプリのベストプラクティス感のあるMVVMをベースとしてる
  • DIなどは他のライブラリ等使用すればもう少しスマートに出来るだろうがあえて使用してない
    • JSONのパース等についても同様‥
    • Add-to-appでNativeアプリの一部分だけをFlutter化したので極力外部ライブラリを使用したくないという事情もある
  • クリーンアーキテクチャも意識してる
  • そんなに大きな機能を実装したわけじゃないので大きくなると色々不都合が出てくるかも‥

大枠

解説

図の上から

View

  • いわゆるUIの部分。StateFullWidgetやStatlessWidgetやら。

Model

  • アプリのコアビジネスロジック。依存の中心。外側の世界については知らない。
  • ChangeNotifierを継承している。
  • 公式で言うところのAppState
  • StateFullWidgetやWiegetツリーの途中に設置されているEphemeralStateもあったりするのでちょっとややこしい

Repository

  • DBやAPIServer等の情報源を隠蔽するレイヤ。
  • Modelから明確にコールするのはRepositoryのみ。

DbService, WebService

  • 主な役割としてRepositoryが要求している形式にデータを整形するレイヤ。
  • WebServiceであればサーバからのJSONレスポンスを変換したり。
  • NativeClientはServiceを挟んでないが、挟んでもいいかもしれない。

DbClient, WebClient, NativeClient

  • DBやAPIサーバとやり取りするレイヤ。

参考実装

チームを作成してチーム内で歩数を競い合う機能のチームを作成する部分の参考実装案を示す。
チームの情報はサーバ上で管理されているので、作成する際にサーバに作成要求を出す。

Model

アプリの中心はModelなのでまずはModelから。

model.dart
class TeamModel extends ChangeNotifier {

  TeamModel(TeamRepositoryInterface teamRepo) {
    _repository = teamRepo;
  }

  void registerTeam(RegisterTeamRequest registerTeamInfo) {
    teamData = null;
    _repository
        .registerTeam(registerTeamInfo)
        .then((TeamInfo teamRes) {
      teamData = teamRes.team;
      notifyListeners();
    });
  }
}
interface.dart
abstract class TeamRepositoryInterface {
  // チームの名前等の情報を設定して、成功したらチームの名前等の情報が帰ってくる
  Future<TeamInfo> registerTeam(RegisterTeamRequest team);
}

Model層でRepositoryのInterfaceを定義し注入することによってコールフローはModel→Repositoryだが依存はModel←Repositoryになるようにしてる。
ダミーのRepositoryを注入すればモックのサーバーを使用することも容易い。
ViewとModelの関係についてはProviderのnotifyListeners()に応じてUIが勝手に変わるのでModelはViewの存在をまったく意識していない。

Repository

repository.dart
class TeamRepository implements TeamRepositoryInterface{
  WebServiceInterface _api;

  TeamRepository(WebServiceInterface apiService) {
    _api = apiService;
  }

  @override
  Future<TeamInfo> registerTeam(RegisterTeamRequest team) async {
    TeamInfo response = await _api.registerTeam(team);
    return response;
  }
}

シンプルですね。
シンプルすぎてもしかしたらJSON等のパースはRepository層でやってもいいのかも?
 →いや、RepositoryにJSONの処理を書くとRepositoryが増えてくると同じようなJSONの処理が各種Repository入るからこれでいいのか。

WebService

service.dart
class WebService implements WebServiceInterface{
  WebClientInterface _client;

  WebService(WebClientInterface client) {
    _client = client;
  }

  Future<TeamInfo> registerTeam(RegisterTeamRequest info) async {
    // _registerTeamToJson()でRegisterTeamRequestの情報をJSONに
    final http.Response response = await _client.postData(
        "api/url", _registerTeamToJson(info));

    // WebClientからはRawJSONとかが帰ってくるのでTeamInfo型にパースしたり...
  }
}

WebClient

このレイヤは基本ただのHttp通信ですね

webclient.dart
class WebClient implements WebClientInterface{
  Future<http.Response> postData(
      String url, Map<String, dynamic> requestData) async {

    var reqBody = jsonEncode(<String, dynamic>{
      // APIの仕様に合わせたBodyを作成
    });

    http.Response response;
    try {
      response = await http.post(
        API_SERVER_URL + url,
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
        body: reqBody,
      );
      return response;
    } catch (e) {
      SimpleLog.debug(e.toString());
    }
    return response;
  }
}

View

view.dart
class MakeTeamScreen extends StatelessWidget {
        // チーム作成フォーム「作成」ボタンコールバック
        onPressed: () {
          if (_formKey.currentState.validate()) {
            _formKey.currentState.save();

            RegisterTeamRequest register = RegisterTeamRequest(
                // チーム名等の必要な情報を設定
            );
            TeamModel teamModel =
                Provider.of<TeamModel>(context, listen: false);
            teamModel.registerTeam(register);
          }
        },
}

TeamModelに作成要求を出す。作成が完了したらnotifyListeners()が呼ばれるので、チームの名前等をUIに反映する。
ModelがViewに対してメソッドやデータ型を提供しているがModelからするとViewがどのようにそれらを扱ってるかは気にしていない。

フォルダ構成

プロジェクトのフォルダ構成は大まかに下記のようになっている。

  • lib
    • view
      • 各種Widget
    • model
      • team_model.dart
      • repository_interface
        • team_repository_interface.dart
    • repository
      • team_repository.dart
      • service_interface
        • web_service_interface.dart
    • web
      • web_service.dart
      • web_client_interface
        • web_client_interface.dart
      • web_client.dart
    • main.dart
    • model_builder.dart

modelから下(外側)に向かってそれぞれ内側でInterfaceを定義して外側で実装することで依存が内側に向かうようになっている。
web配下のweb_client_interfaceあたりはちょっとやりすぎかもしれない。。

依存の注入

main.dart
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
            create: (context) => ModelBuilder.buildTeamModel()),
      ],
      child: MaterialApp(
      // ・・・
model_builder.dart
  static TeamModel buildTeamModel() {
    WebServerClient client = WebServerClient();
    WebService service = WebService(client);
    TeamRepository repo = TeamRepository(service)
    return TeamModel(repo);
  }

Modelへの依存の注入はmodel_builder.dartにて泥臭く。
ProxyProviderを使用してもできそうだがModelからAPIサーバへのレイヤはProviderという強烈なライブラリへの依存を避けるという意味でもしなくて良いと考えている。
buildTeamModel()内部で必要に応じてDummyTeamRepositoryとかを生成すればAPIサーバが無い状態での実装等が簡単に可能である。
もちろん、WebServiceやWebServerClientが複数生成されないようにしたりも必要。