はじめに
こんにちは!グロービスで「ナノ単科」の開発を担当しています、GDPの堀尾です。
GLOBIS Advent Calendar 2023 の17日目として、普段開発で使用しているFlutterについて書いてみたいと思います。
ナノ単科とは?
ナノ単科はグロービス経営大学院が2021年10月から提供している教育プログラムです。動画学習はもちろん、AIを使った学習コンテンツや大学院講師によるライブ授業、受講生同士で学習理解を深めるグループワークなどを通じて、6週間で仕事に活かせる基礎スキルを身につけることができます。
本題
Flutterを勉強してたらよく見るこういうコード。
特にRiverpod関連で。。。
final hogehogeViewmodelProvider = StateNotifierProvider.autoDispose<HogehogeViewmodel, hogehoge>(
(ref) => HogehogeViewmodel()),
);
class HogehogeViewmodel extends StateNotifier<AsyncValue<Hoge?>> {
hogehogeViewmodel() : super(const AsyncValue.loading())
}
ViewModelって何?ViewModelProviderって何?
abstract class HogehogeRepository {
...
}
なんでabstract classで定義してるの?
ってかそもそもRepositoryって何?
class HogehogeRepositoryImpl extends HogehogeRepository {
HogehogeRepositoryImpl();
@override
Future<Hoge> doSomething() async {
...
}
}
実装はImplでって何?
final hogehogeRepositoryProvider = Provider.autoDispose<HogehogeRepository>(
(ref) => HogehogeRepositoryImpl(),
);
RepositoryProviderって何?
ViewModelとかProviderとかRepositoryとかMVVMとかよくわかんねぇよ!
その気持ちわかります。
上記のコードはMVVMパターンとRepositoryパターンを組み合わせて作ってる & RiverpodをつかってDIして作ってる
は?
以下解説
MVVMって何?
ざっくりいうと、
- Model : データの加工、取得、保存などが役割
- View : ViewModelの情報を使用してUIに表示、ViewModelにアクションを送信
- ViewModel : ModelからのUIに描画するための情報を変換・保持する
ViewModelは変更があったデータを即時Viewに反映してくれる
lib
┣ models
┣ viewmodels
┣ views
┣ app.dart
...
普段開発しているナノ単科のディレクトリ階層にも、modelとviewmodelとviewを用意しています。
(※ 現在ナノ単科ではviewmodelという用語は使用していません、notifierにすべて置き換えました。この記事ではMVVMモデルで馴染みの深いviewmodelを使用して解説していきます)
MVVMのメリット
- Viewに素早くデータを反映することができる
- 画面更新の処理をわざわざ書かなくてもいいので、その分他のことに気を使える
- ↑ データバインディングという仕組みのおかげ、気になる人は調べてみて
Repositoryパターンって何?
簡単に言うと、データソースからデータを引っ張ってくる処理は抽象的なレイヤ(Repository)に任せようね、というつくり。
これの何が嬉しいかというと、基本的にデータを扱う側はそのデータがどこから来たかなんてどうでもいいので、それだけ疎結合になるので依存性が下がるわけです。
「データくれ」といってデータをくれればそれで良いわけです。
なのでRepositoryが色んなところからデータを持ってきてくれるけど、ViewModelなんかは出処を知らなくても良い → 疎結合になって嬉しい
実際の現場ではデータソースはバックエンドだったり、外部のAPIだったりするでしょう。
ナノ単科でも色んなところからデータを引っ張ってきています
models
└ repositories
┣ database
┣ graphql
┣ sharedpreference
└ remote
- database
- ブラウザのデータベースからデータを取得する
- ※ ちなHiveというパッケージでブラウザDBを扱える
- graphql
- バックエンドのDBからデータを取得する
- sharedpreference
- SharedPreferenceというストア的なものからデータを取得する
- remote
- いろいろ、上記以外の外部APIとか
MVVM + Repositoryパターン
実はさっき出てきた画像(↑)はMVVMとRepositoryパターンを組み合わせた画像でした。
Flutterでよくあるパターンとして上記のように作っているプロダクトも多いのではないでしょうか。
DIって何?
DIとはDependency Injectionの略で、日本語にすると依存性の注入です。
依存性の注入っても何がなんだか分かんねぇよ、という感じですね。
簡単に言うと、
「dependency」は使われるオブジェクト(サービスと呼ぶ)であり、「injection」とはそのオブジェクト(=サービス)を、それを使うオブジェクト(クライアントと呼ぶ)に渡すことです。
です。あるクラスで使うオブジェクトを外部から渡してやろうということです。
DIじゃない例
あるサービスクラスがあるとします。
class Service{
void doSomething(){
print('hogehoge');
}
}
このとき、クライアントクラスでサービスクラスを扱うときに、以下のような書き方をすることがあると思います。
class Client{
Service _service;
Client(){
this._service = new Service();
}
void doSomething(){
_service.doSomething();
}
}
この場合、ClientクラスのコンストラクタでServiceクラスを呼び出しているので、ClientクラスはServiceクラスに依存していると言えます(密結合)
DIしてみる
ちょっとDIしてみましょう。
先程のClientクラスを以下のように書き換えます。
class Client{
Service _service;
Client(this._service);
void doSomething(){
_service.doSomething();
}
}
これだと、外からServiceクラスを渡してくれているので、依存性がちょっと下がりました。
ちなみにClientクラスを使う時は以下のような感じになります。
void main() {
final service = Service();
final client = Client(service); // ServiceをClientに渡す(注入)
client.doSomething(); // hogehoge
}
外からServiceクラスを渡していますね。
もうちょいDIしてみる
先の例では外からオブジェクトを渡していましたが、これだとServiceクラスしか渡せないという問題があります。Service2とかが渡せないわけです。
もうちょい依存性を下げてみましょう。
まずは、Serviceクラス用のインターフェースを用意します。
abstract class ServiceInterface {
void doSomething();
}
このインターフェースをつかってServiceクラスを実装してみます。
class Service extends ServiceInterface {
void doSomething() {
print('hogehoge');
}
}
プラスでService2も作ってみましょう
class Service2 extends ServiceInterface {
void doSomething() {
print('hogehoge2');
}
}
Clientクラスをちょっと書き換えます
class Client {
ServiceInterface _service; // ← ServiceInterfaceを受け取るようにする
Client(this._service);
void doSomething() {
_service.doSomething();
}
}
すると
void main() {
Client(Service()).doSomething(); // hogehoge
Client(Service2()).doSomething(); // hogehoge2
}
ServiceクラスもService2クラスも注入できるようになりました。
何が嬉しいの?
なんでこんな作り方をするか?ですが、
ここで、上記のServiceクラスが外部からデータを取得するクラスだとイメージしてみてください。
class Service extends ServiceInterface {
void doSomething() {
// HTTP通信とかで外部からデータを取得する
}
}
さらに、「Clientのテストをしたい」場合、この作りが便利になってきます。
例えば、Service2クラスをモックにすると、
class Service2 extends ServiceInterface {
void doSomething() {
// Jsonとかからテストデータを取得する
}
}
ClientクラスにService2クラスを渡すだけで、外部と通信をしなくてもテストデータでClientクラスのテストができるわけです。
Clientクラスは正常なデータが渡ってきさえすればロジックのテストができるので、データがどこから渡ってくるか知ったこっちゃないわけです。
この感じ、さっきもRepositoryパターンのところで見ましたね。
なんでRepositoryはabstract classで定義してるの?
さて本題です。
abstract class HogehogeRepository {
...
}
class HogehogeRepositoryImpl extends HogehogeRepository {
HogehogeRepositoryImpl();
@override
Future<Hoge> doSomething() async {
...
}
}
Repositoryがなぜabstract classで定義されているかというと、DIを行うためというわけでした。
さっきの例で言うServiceInterface
が上記のコードで言うHogehogeRepository
、ServiceクラスがHogehogeRepositoryImpl
に対応しています。
テストをしたい場合は、HogehogeRepositoryImpl
をモックにしたりすることで、わざわざ外部通信(GraphQLとか使ってバックエンドのDBから値を取得)することなく、テストデータでテストできるという利点があります。
〇〇Providerって何?
もうちょっとDIの話をしてみます。
final hogehogeViewmodelProvider = StateNotifierProvider.autoDispose<HogehogeViewmodel, hogehoge>(
(ref) => HogehogeViewmodel()),
);
class HogehogeViewmodel extends StateNotifier<AsyncValue<Hoge?>> {
hogehogeViewmodel() : super(const AsyncValue.loading())
}
final hogehogeRepositoryProvider = Provider.autoDispose<HogehogeRepository>(
(ref) => HogehogeRepositoryImpl(),
);
Flutterのコードを見てると、〇〇Providerってのがよく出てくると思います。
〇〇ViewModelProviderとか〇〇RepositoryProviderとか、後ろにProviderってのがついたオブジェクトがよく出てきます。
Providerとは何か?
Providerとは、Riverpodで使用される状態を監視するためのオブジェクト的なものです。監視と言いましたが、簡単に言うと「値が変更されたらお知らせしてくれる」みたいな感じです。
上記のような書き方は、Riverpodのお決まり的な書き方になります。
Riverpodはアプリの状態管理に使われるパッケージで、状態(値)が変わると自動的に通知し、画面を再描画してくれるというものです。
Providerを使うことで、アプリの様々な場所(ほぼどこからでも)から状態(値)にアクセスできるようになります。
こんな感じで
ref.watch(hogehogeViewModelProvider)
アプリの様々な場所(ほぼどこからでも)というのがミソで、Riverpodの前身となるProviderというパッケージ(名前がややこしいですが、この章で解説しているProviderとは別物です。パッケージ名です)では、アクセスできる場所が限られていたのですが、Riverpodではどこからでもアクセスできるようになりました。
Providerに実装を注入
よくあるパターンとして、hogehogeRepositoryProvider
に、hogehogeRepositoryImpl
が注入していたりします。
final hogehogeRepositoryProvider = Provider.autoDispose<HogehogeRepository>(
(ref) => hogehogeRepositoryImpl(),
);
これもいわゆるDIになっています。
hogehogeRepositoryProvider
に対して、hogehogeRepositoryImpl
という実装クラスを注入してくれているわけです。
これによって、RepositoryImpl
をモックなどに差し替えたりするとテストがしやすいというメリットが生まれます。
参考