こんにちは!YOUTRUSTのアプリエンジニアの朝日(YOUTRUST/Twitter/ブログ)です。技術的側面でアプリ開発をリードしています。
ちなみに、僕はボードゲームが大好きなのですが、ここ2年くらいほぼ出来てなくてとても悲しい日々を過ごしています。いろいろ落ち着いたら僕と遊んでくださる方もゆるく募集してます笑
さて、今回の記事ではYOUTRUSTアプリのレイヤー構成について話そうと思います。
YOUTRUSTのアプリは2021年4月12日に生まれました。
https://prtimes.jp/main/html/rd/p/000000031.000040832.html
使っていただいている方々はありがとうございます。
ダウンロードまだだよ!と言う方はぜひダウンロードしていただけると嬉しいです!
📌 大前提
さて、今回の記事を解説するにあたり、大前提としてYOUTRUSTアプリはFlutterで作られています。
また、flutter_hooks | Flutter Package
とhooks_riverpod | Flutter Package
パッケージを使っています。
👨💻 対象読者
本記事では、
- 会社でFlutterでアプリ作ることが決まったんだけど、どんな構成で作れば良いか分からないよ
- みんながどんな構成でアプリを作っているか知りたいよ
な方を対象に書いています。
アプリの構成に詰まった時、皆様の一助になる様な記事になると嬉しいです。
また、以前勉強会で発表した資料も載せておきますのでこちらを読みながらだとより一層本記事の理解がしやすくなるかなと思います。
TL;DR
ViewModel
,Facade
,Store
に分かれているViewModel
は、その画面で行いたいことがメソッド単位で定義されておりScreen
から呼ばれる。Facade
を呼ぶだけになることが多い。Facade
はある機能の一連のデータの流れを担保する事を責務とする。 APIを投げ、その結果を後述するStore
を介してメモリ上などに保存することが多い。Store
はProvider
で保持している値を更新したり取得したりすることを責務とする。
🌈 大枠・共通言語
共通言語
Scaffold
を持つウィジェットをScreen
と呼んでいます。
🧩 各レイヤーの責務など
Screen
上にも書いたようにScaffold
を持つWidget
をScreen
と名付けています。
Screen
は(State)Provider
をref.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っぽい書き方でない?
FutureProvider
やStateNotifierProvider
を使っていなかったりして、
riverpod
のメリットを活かしきれていない感覚はあります。
なので、もう少し稼働に余裕が出来たら(出来るのか?w)この辺の改善も出来る限り着手したいななんて思っています。
Riverpodのポテンシャルを活かしきれていない
上記の補足の様になりますが、Riverpod
パッケージには様々な優れたProvider
があります。
弊社の構成だと取得部分をFacade
がしてしまうので、FutureProvider
の出る幕がありません。
FutureProvider
を使えば、取得中/エラー/成功時のUIの構築がスムーズに出来るため使えると良いなぁと思っています。
たまにViewModelが冗長に感じる時がある
今回紹介した中で、一番処理を持っていないのは多分ViewModel
です。
Screen
が頑張らないためだけに存在している気はしています。
そういったところもあり、たまにViewModel
を作っている時に何か代替案とかないかななんて考えてしまう時もありました。
とはいえ、全体的な統一感だったりこのビューだけこうしたいみたいな要件もなきにしもあらずなので引き続き作り続けています。
🎨 最後に
YOUTRUSTアプリの構成および課題に感じている部分をざっと書いてみました。
会社としても、自分としてもアプリでやりたいことがまだまだたくさんあります。
また、このブログを読んでいただいた方の中には自分が改善したい!なんて思ってくれた方もいらっしゃるかもしれません。
弊社では絶賛Flutterエンジニアを募集していますので、まずはカジュアル面談からでもご応募いただけると嬉しいですっ!