Flutter製YOUTRUSTアプリを支えるレイヤー構成

こんにちは!YOUTRUSTのアプリエンジニアの朝日(YOUTRUST/Twitter/ブログ)です。技術的側面でアプリ開発をリードしています。

ちなみに、僕はボードゲームが大好きなのですが、ここ2年くらいほぼ出来てなくてとても悲しい日々を過ごしています。いろいろ落ち着いたら僕と遊んでくださる方もゆるく募集してます笑

手持ちのボードゲームの一部


さて、今回の記事ではYOUTRUSTアプリのレイヤー構成について話そうと思います。

YOUTRUSTのアプリは2021年4月12日に生まれました。

https://prtimes.jp/main/html/rd/p/000000031.000040832.html

使っていただいている方々はありがとうございます。

ダウンロードまだだよ!と言う方はぜひダウンロードしていただけると嬉しいです!

📌 大前提

さて、今回の記事を解説するにあたり、大前提としてYOUTRUSTアプリはFlutterで作られています。

また、flutter_hooks | Flutter Packagehooks_riverpod | Flutter Packageパッケージを使っています。

👨‍💻 対象読者

本記事では、

  • 会社でFlutterでアプリ作ることが決まったんだけど、どんな構成で作れば良いか分からないよ
  • みんながどんな構成でアプリを作っているか知りたいよ

な方を対象に書いています。

アプリの構成に詰まった時、皆様の一助になる様な記事になると嬉しいです。

また、以前勉強会で発表した資料も載せておきますのでこちらを読みながらだとより一層本記事の理解がしやすくなるかなと思います。

speakerdeck.com

TL;DR

  • ViewModel, Facade, Storeに分かれている
  • ViewModelは、その画面で行いたいことがメソッド単位で定義されておりScreenから呼ばれる。 Facadeを呼ぶだけになることが多い。
  • Facadeはある機能の一連のデータの流れを担保する事を責務とする。 APIを投げ、その結果を後述するStoreを介してメモリ上などに保存することが多い。
  • StoreProviderで保持している値を更新したり取得したりすることを責務とする。

🌈 大枠・共通言語

youtrust_app_layers

共通言語

Scaffoldを持つウィジェットScreenと呼んでいます。

🧩 各レイヤーの責務など

Screen

上にも書いたようにScaffoldを持つWidgetScreenと名付けています。

Screen(State)Providerref.watchで監視しています。

また、ボタンがタップされた際の挙動などはViewModelに伝えて処理をしてもらいます。

ViewModel

Screenの会話相手です。

そこまで多くのロジックは持っておらず、基本的には後述するFacadeとの仲介役になることが多いです。

ちなみに、View単位のみでしか使用せず、画面がなくなったら必要ないことが多いため

final _viewModeProvider = Provider.autoDispose(
  (ref) => HogeViewModel(ref.read),
);

Screenと同じファイルに定義しています。

Facade

ある機能におけるデータの一連の流れを正しく操作する責務を持っています。

例えば、Twitterのフォロー機能を想像してみてください。

Twitter上でフォローと言うのは様々な画面で行うことができますよね?

ユーザ画面、おすすめユーザ一覧...などなど。

上記ViewModel上にフォロー/アンフォローなどのロジックを置いてしまうと

各所で重複してしまいます。

そんな時にFacadeを以下の様に定義し、各ViewModelから呼びます。

class UserFacade {
  static Future<void> follow({
    required Reader read,
    required String userId,
  }) async {
    // フォローAPIを呼ぶ

    // フォローしたことにより、自身のフォロー数や相手のフォロワー数など
    // メモリ内情報に変更が発生することがあるため更新したり...
  }
  ...
}

Store

ざっくり言うとriverpodのラッパーになります。

今度は、Twitterのフィードを想像してみてください。

List<Tweet>の様なものを取得すると思うのですが、これをProviderに保存するために

以下のようなコードになっています。

(もちろん、実際は重複制御などもう少し丁寧に扱うと思いますが)

class TweetStore {
  static void append({
    required Reader read,
    required List<Tweet> tweets,
  }) {
    read(_tweetsStateProvider.state).update(
      (List<Tweet> oldState) => [
        ...oldState,
        ...tweets,
      ],
    );
  }
  ...
}

このStoreによるProvider更新によって、最初に言及したScreenが再描画されると言う構造になっています。

ちなみに、このレイヤーが必要かと言う意見もあるかと思いますが、個人的には助かってる部分は大きく2点あると思っています。

一番大きいのは、Providerの構成を変える時に変更がStore内に閉じやすいことです。

二つのProviderを一つにまとめたい、またはその逆みたいなリファクタリングをしたい時に

Providerを直接参照している箇所が主にStore内だけなので変更しやすいです。

また2つ目として、メモリ内データの制御に重点を置いたテストができることです。

データの変更に関しては、かなり大事な部分なので重点的にテストを書きたい部分になると思います。

そこが一つのレイヤーになっていることによって、データ制御部分のみにフォーカスしたテストが書きやすくなっています。

ちなみに、Storeのテストが十分に行われている場合、Facadeのテストを書くときは状態テストではなく呼び出しテストだけで十分かなと思っています。

🎯 今感じている課題

さて、大雑把に弊社のアプリのレイヤー構成について解説してきました。

1年半以上アプリを開発してきて、この構成で無理が出たことがないと言う意味ではある程度安定した構成になっているのかなと思っています。

一方で、今から一からアプリを作れと言われてもこの構成にするか、と聞かれたら少々悩ましいところもあります。

そんな悩みについて少し書き足したいと思います。

そもそもFlutter + Riverpodっぽい書き方でない?

FutureProviderStateNotifierProviderを使っていなかったりして、

riverpodのメリットを活かしきれていない感覚はあります。

なので、もう少し稼働に余裕が出来たら(出来るのか?w)この辺の改善も出来る限り着手したいななんて思っています。

Riverpodのポテンシャルを活かしきれていない

上記の補足の様になりますが、Riverpodパッケージには様々な優れたProviderがあります。

弊社の構成だと取得部分をFacadeがしてしまうので、FutureProviderの出る幕がありません。

FutureProviderを使えば、取得中/エラー/成功時のUIの構築がスムーズに出来るため使えると良いなぁと思っています。

たまにViewModelが冗長に感じる時がある

今回紹介した中で、一番処理を持っていないのは多分ViewModelです。

Screenが頑張らないためだけに存在している気はしています。

そういったところもあり、たまにViewModelを作っている時に何か代替案とかないかななんて考えてしまう時もありました。

とはいえ、全体的な統一感だったりこのビューだけこうしたいみたいな要件もなきにしもあらずなので引き続き作り続けています。

🎨 最後に

YOUTRUSTアプリの構成および課題に感じている部分をざっと書いてみました。

会社としても、自分としてもアプリでやりたいことがまだまだたくさんあります。

また、このブログを読んでいただいた方の中には自分が改善したい!なんて思ってくれた方もいらっしゃるかもしれません。

herp.careers

弊社では絶賛Flutterエンジニアを募集していますので、まずはカジュアル面談からでもご応募いただけると嬉しいですっ!