はじめに
この記事は、MVVMパターンの代表として語られるVue.jsを使ってwebアプリ開発をしているエンジニアが、MVVMパターンを用いてサンプルのflutterアプリを開発してみるもの。
もともとflutterでのアプリ開発でMVVMパターンが採用されることは少なくない。
なぜならAndroidアプリの開発ではMVVMパターンが推奨されており、AndroidエンジニアがFlutterでもスムーズに開発できるようにするためだ。
一方で筆者はAndroidアプリエンジニアではないが、同じくMVVMパターンのフレームワークであるVueのエンジニアである。
そのため、Flutterでのアプリを作成するにあたり、Vue.jsで作り慣れた構成にてアプリを作ってみよう、と思ったのだ。
この記事の実装内容は、以下のgithub repositoryに作成していく。
Flutterアプリ+MVVMパターンの参考記事:
そもそもどんなパターンがあるの?
アーキテクチャパターン
そもそもFlutter開発のアーキテクチャパターンの選択肢としては、大別すると以下のようなものがあるらしい。
- MVVMパターン
- Reduxパターン
- mobXパターン
MVVMパターン
以下の記事で解説した。
一言で言うと、MVCパターンの、CとVがデータのやりとりではなくdatabindをしているもの、というイメージ。
Reduxパターン
Vue.jsエンジニア的には、以下の記事がわかりやすい。
MVVMとFluxの違いは、データの流れを一方向に制限されているかどうかという点。
で、ReduxはFluxの亜種的なもの、くらいの理解。
mobXパターン
以下の記事を読むといい。
MobXはReduxと同じくJavascriptのために考えられた設計で、主にReactと共に使われ
ているらしい。
状態管理パターン
で、これらのアーキテクチャパターンを実現するための、つまりdatabindを実現するための状態管理の方法として、以下のような選択肢があるらしい。
伝達の受信 | 状態の伝達(保存?) |
---|---|
StatefulWidget | StatefulWidget |
InheritedWidget | InheritedModel |
ChangeNotifier or ValueNotifier | Provider or Riverpod |
StateNotifier + freezed | Provider or Riverpod |
BLoC | Provider or Riverpod |
状態管理パターンの選定
今回は、複数のWidgetから同じ変数にアクセスしたいので、StatefulWidgetは除外。
ある状態変数を2つ以上のWIdgetが変更したり、子Widgetで状態変数が変更されて親Widgetでその状態変数を利用するような場合は、package:provider + ChangeNotifier、あるいはpackage:provider + BLoCなどを使った方が管理しやすいです。
出典:Flutterアプリの主流な状態管理パターンと導入事例まとめ(2020年版)#パターン1-StatefulWidget
InheritedWidget使うならProviderを使え、と言う言説が至る所にあるのでInheritedWidgetは除外。
inheritedWidgetはわかりにくくコード量も無駄に多くなるということで、package:providerが登場しました。現状、package:providerでできるような用途の場合、package:providerを使いましょう。
出典:Flutterアプリの主流な状態管理パターンと導入事例まとめ(2020年版)#パターン2-InheritedWidget--InheritedModel
ChangeNotifierかStateNotifierかでいうと、値はimmutableにしたいので、StateNotifierを採用。
mutableで簡単に済ませたいし、それで問題を感じない → ChangeNotifier
状態クラスの扱いが多少煩わしくなるのを許容してでもimmutableの恩恵を得たい → StateNotifier
ProviderかRiverpodかでいうと、あえてProviderを使う理由もないのでRiverpodを採用。
純粋に機能的にProviderがRiverpodより優れている点は思い浮かびません。そのため、個人的には今後Providerをあえて選ぶことはないと思います
と言うことで、今回は
StateNotifier + freezed + Riverpod
と言う構成でアプリを作ってみる!
StateNotifier
かChangeNotifier
かは、両方作ってみて使いやすいのはどちらかを考えてみる。
実際に作成
参考にする記事は、以下たち。
MVVMの参考記事
StateNotifier + freezed + Riverpodの参考記事
ちなみにflutterのversionはこんな感じ。
$ flutter --version
Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 77d935af4d (5 weeks ago) • 2021-12-16 08:37:33 -0800
Engine • revision 890a5fca2e
Tools • Dart 2.15.1
以前stableではないchannelを用いてしまったせいでハマったことがあるので、確認を怠らないようにしないと。
プロジェクトの作成
ファイルの分割
この最初のページを書き換えていく。
とりあえず、ファイルを分割してフォルダを整理。
該当コミット: 686fa79f27d4eab3fec75fe3f71291fceb704260
Providerデザインパターンの採用
ViewModelを作成
ChangeNotifierを導入することで、stateful widgetをstateless widgetに書き換える。
これをすることで、状態を保持する変数を、別ファイルに記述することができるようになるのが嬉しい。
見た目を記述する部分と値のロジックを記述する部分の分離。
該当commit: ae222520013ec557a807acadb5014581a1c3002e
Riverpod + Hooksの導入
ViewModelを更新
StateNotifierとHookWidgetを用いて、ChangeNotifierで記述していた部分をより簡略化して書けるようになった。
StateNotifierの導入: 46af4f55be8abc554549ab460494fa8f17537b59
Hooksの導入: b84eba716b959dbbba8af7311888809441312913
上記の記事だと、riverpodのversionが0.14.0+3
だったので、^1.0.0
に更新する。
更新commit: e45d4a8e0bca15ca315d515fa6ee97d4c14ff8b3
ちなみに、Riverpodのパッケージには以下のような種類がある。
(出典:Flutter#2 〜Flutterの状態管理パターン (Riverpod)〜)
freezedの導入
freezedは、immutableなクラスを作成するためのpackage。
このパッケージが必要な理由は以下。
理由1
Flutterの中で状態管理をしても、たまに値の変更が検出されず画面が更新されないことがある。
Flutterの状態管理では、参照先の「インスタンスが変わったか」によって、値の変更を検出する。しかし、インスタンス内で値が変わっても、そのインスタンスが変更されないため、状態管理としては、「値は変わっていない」と判断される。
出典: 【Flutter/Dart】状態管理に必要なfreezedパッケージを徹底解説 クラスの作成・生成方法からチップスまで【2021年12月版】
一言で言うと、「インスタンスを変更することで再レンダリングを正しく行わせるため」。
理由2
ChangeNotifierでは、状態の変更を通知するために、状態を変更するたびに notifylisteners関数を呼ぶ必要があります。この煩わしさを解消してくれたのが、StateNotifierになります。
StateNotifierは、一つの状態(state)しか持つことができません。そのため、複数の状態を管理したい場合は、オブジェクトを作成して管理することになります。
(中略)
StateNotifierで利用するState用のクラスをimmutableにすると、StateNotifier側の記述が冗長になってしまいます。これを解決するのがfreezedパッケージになります。
出典: Flutter#2 〜Flutterの状態管理パターン (Riverpod)〜
一言で言うと、「StateNotifierの記述を簡潔にするため」。
他にも、色々freezedの説明をしてくれている記事は以下。
さて、これらの記事を参考に、アプリにfreezedを導入していく。
現状ではクラスを変数として保持していないので、modelを追加してそのmodelクラスを保持するようにコードを修正する。(このmodelはMVVMパターンでいうところのModelとは別)
更新commit: b4aff7792570bfa8b41d4144afe3ab8970941b00
その後、保持されたmodelをimmutableとするよう、state(MVVMでいうところのModel)ファイルを作成し、stateファイルにfreezedを導入する。
更新commit: fec7777642ea3caf55be45e5c5c149a404a7c2a3
まとめ
と言うことで、アプリの挙動自体は変わっていないが、デフォルトアプリのMVVMパターンへの書き換えが完了した。
最終的なフォルダ構造は以下。
/state
-> MVVMパターンでいうところのModel。VueでいうところのStoreファイル。
/ui/home_view.dart
-> MVVMパターンでいうところのView。VueでいうところのViewファイルのhtml部分。
/ui/home_view_model.dart
-> MVVMパターンでいうところのViewModel。VueでいうところのViewファイルのscript部分。
/model
-> ただのデータモデル。MVVMパターンのModelとは別。Vueでいうところのmodel。
/provider
-> ViewModelの変更をViewに検知させるためのもの。Vueでデータバインドは勝手にやってくれてる。
これで、最小のMVVMパターンでのアプリが完成した。
MVVMパターンを採用してアプリを作成するとはどういうことか、が理解できた気がする。
まだ通信を伴ってはいないので今後はこれにrepositoryを導入していくことも必要だが、一旦MVVMパターンを理解した、ということで一区切り。