どうも、株式会社YOUTRUSTのアプリ開発のリードエンジニアをやっているashdikこと朝日(YOUTRUST / X)です。
最近は、中量級のソロでも出来るボードゲームにハマっています。
EARTH、アンドールの伝説、エバーグリーン、The guild of merchant explorers、Dune Imperium、
アルナックの失われし遺跡、イーオンズ・エンド...などなど数々やってきました。
が、圧倒的にハマっているのが、スピリットアイランド。
一回1時間かかるのに12月くらいからほぼ毎日やってます。
楽しすぎて出会えたことに本当に感謝です。
⚓️ 概要
第一部は、こちらの記事でYOUTRUSTアプリのレイヤー構成についてざっと説明しました。
第二部は、レイヤー構成の記事に登場する図のうち、ApiClient
及び Request
を説明しました。
第三部は、レイヤー構成の記事に登場する図のうち、Store
を説明しました。
第四部は、レイヤー構成の記事に登場する図のうち、 Facade
を説明しました。
今回は、第五部と称して、 ViewModel
について触れていこうと思います。
💻 筆者環境 (執筆時)
name | version |
---|---|
Flutter | 3.13.9 |
Dart | 3.1.5 |
flutter_hooks | 0.20.3 |
hooks_riverpod | 2.4.5 |
📓 大前提(お詫び)
(前回に引き続き)
riverpod
は黎明期の頃から使っており、 FutureProvider
は使われておらず StateNotfierProvider
も最近導入し始めたところです。
その辺の構成の参考にしようとしている方には、もしかしたらお役に立てないかもしれません。
ですが、2年半くらい今の構成で作り続けており、非常に作りやすいと感じています。
また、新しくジョインしていただいた方々にもコードが読みやすいと好評いただいているので
一つのパターンとしてありなのではないかなと個人的には思っています。
それではそんな言い訳をしたところで笑、本編の解説に入りたいと思います。
ViewModel
概要
弊社での ViewModel
の責務は以下です。
- 画面の会話相手
- 適切な
Facade
の呼び出し
実装イメージ
class UserViewModel { const UserViewModel( this._read, { required this._userId, }); final Reader _read; final UserId _userId; // 画面表示時に一度だけ行う処理を初期化処理 Future<void> initState() async { await _fetchUser(); // ログの送信 TrackingFacade.log( read: _read, request: UserAccessRequest(userId: _userId), ); } // 画面を離れる際の処理 void dispose() { } // 状態の再取得 (PullToRefresh) Future<void> reloadState() async { await _fetchUser(); } // 最新情報の取得と先頭への追加 Future<void> peekState() async { await _fetchUser(); } Future<void> _fetchUser() async { // ユーザ情報の取得 await UserFacade.fetch( read: _read, userId: _userId, ); } }
解説
Readerについて
Facade
や Store
内での操作で、 Reader
を使っているので保持しています。
initState
画面の表示時に、一度だけ呼ばれます。
主に、
- その画面の表示に必要なデータの取得
- 表示ログの送信
などの処理がよばれることが多いです。
dispose
画面の非表示(破棄)時に、一度だけ呼ばれます。
とはいえ、hooks
の恩恵もあり、ここに処理が記述されることは多くないです。
reloadState
PullToRefresh
など、画面全体のデータの再読み込みの際の処理を記述します。
peekState
バックグラウンドからの復帰時など、最新情報を取得し、既存データとの差分だけを先頭に追加する処理を行います。
fetchUser
Future<void>
になっているところだけ少し補足します。
前回までの記事を読んでいただけた方はわかっていただけると思いますが、 Facade
内で取得したものは
Store
内にある StateProvider
によって保持されます。
なので、このメソッド自体は Future<void>
として返り値なしとなっています。
その他
1画面 1ViewModel
動的な画面であれば、基本的に1画面1ViewModel
の対応で作成しています。
shallow
なクラスを作ってしまっているな、という実感も少しありますが、以下の2点の理由から作成するようにしています。
- 画面の対話相手を
ViewModel
だけにしたい - 画面特有の処理を行うクラスが欲しい (ログなど)
- アプリ全体の構造として統一感を持たせたい
画面固有パラメタの保持
ユーザ詳細画面を想像してみてください。 必ず、どのユーザIDかという識別子が必要になってきます。
その情報も ViewModel
は保持しています。
class UserViewModel { const UserViewModel( this._read, required this.userId, }); final Reader _read; final UserId _userId; Future<void> follow() async { UserFacade.follow( read: _read, userId: _userId, ); } ... }
この _userId
を利用することにより、各種メソッドの引数に userId
を渡さなくて
済むようになっています。
また、お気づきかもしれませんが UserId
型になっています。
本来であれば、String
型なのですが型安全にするために UserId
型を定義しています。
これにより、確実にユーザーのIDを引数として受け取ることが可能になります。
bool isFollowing({required UserId userId}) => ...; // GOOD isFollowing(user.id); // compile error isFollowing(post.id);
🎥 最後に
いかがでしたでしょうか?
このシリーズも、とうとう第五部!
全部読んでくれているかたは、ある程度弊社のアプリケーションコードの仕組みについて理解が深まっているのではないでしょうか。
エンジニア募集中っ!
- このシリーズを読んで、実際に触ってみたい!と思っていただけた方
- 純粋にYOUTRUSTに興味がある方
YOUTRUSTでは、エンジニアを募集しています!
ぜひ、下のリンクよりご応募お待ちしておりまっす!