Unity(C#)からFlutterに転向してハマった落とし穴 ~Flutter開発で気にしなくていい3つのこと~
こんにちは。株式会社SODAでFlutterエンジニアをしているTPと申します。私は前職までUnityエンジニアをしており、Flutter歴は独学の時期も含めると3年ほどになります。
今回は、Unityエンジニアだった私がFlutterを学ぶにあたってハマった落とし穴を紹介します。特に、他の開発プラットフォームにすでに習熟していたからこそハマってしまった点を紹介していきます。
私と同じように、Flutter以外の開発プラットフォームから転向してくる方々の参考になれば幸いです。また、他のプラットフォームの事情も知りたい方や、Flutterのエコシステムを支えている概念に興味がある方にとっても価値のある記事にしたいと思います。
また、こちらの記事は、先日行われた第8回 Flutter Gakkaiの発表を元にしております。スライドの方も合わせてご覧いただくとより理解いただけるかと思います。
気にしなくてよい3つのこと
Flutterによる開発を2年続けてみて、私が「これはもう気にしないほうがよい」と思ったポイントが3つあります。順番に紹介していきます。
ヒープメモリ
Flutterを取り巻くエコシステムは、Unity/C#のそれよりもかなり富豪的な設計になっています。特にヒープメモリに関しては、開発者がコントロールすることはかなり難しいです。
RiverpodのProviderで管理するオブジェクトはimmutable(不変)である必要があります。これはオブジェクトのコピーや部分的な変更の際に、常にヒープメモリアロケーションが発生することを意味しています。Providerでオブジェクト同士の依存関係を構築していると、各変数の生成・破棄についてもProviderに一任する形になるので、開発者がそれらをコントロールすることは難しいです。
Flutterを学び始めた自分にとっては、Providerのこの特性は奇妙なものに思えました。サイズの小さいオブジェクトならまだしも、配列などのサイズの大きなオブジェクトを頻繁にコピーしていてはメモリのパフォーマンスが下がってしまうからです。
しかし、Flutterでの開発を続けるにつれ、そんなことはほとんど気にしなくていいのだと分かりました。モバイルアプリ開発において、状態が更新されるのは、ユーザーが操作をするか、サーバーからイベント更新が伝達された時に限られます。ほとんどのアプリにおいて、それらの頻度は多く見積もっても1秒に数回程度です。よって、ヒープメモリを気にしたパフォーマンスチューニングが必要になる場面はFlutter開発においてほとんど発生しません。
依存性の逆転
プログラムのアーキテクチャを考える時、モジュール同士の依存関係は大切な要素の一つです。プログラムの依存関係を設計するとき、より本質的で変更の少ない部分を依存先に置き、外的要因によって変更されやすい部分(UIや通信部など)を依存元にすることがよいとされています。
愚直にプログラムを実装すると、関数の呼び出し元が呼び出し先に依存することになります。しかし、モジュールの依存関係を整理するにあたって、呼び出し先を呼び出し元に依存させたい場合も出てきます。これを解決するのが依存性の逆転です。これは多くのプログラミング言語においてインターフェースという機能で提供されています。
C#は特にインターフェースを駆使する言語です。明示的にインターフェースを定義し、それによって依存関係をコントロールするのがよいプラクティスとされています。なので、私がFlutter開発を始めた時はインターフェースをよく実装していました。例えば、リポジトリのインターフェースをドメインレイヤーに定義し、リポジトリモジュール内にその実装を書くなどしていました。
しかし、Flutter開発に慣れるにつれ、これらのインターフェース定義にあまりメリットがないことがわかってきました。
Dartの言語仕様において、クラスを定義したときにそのインターフェースも自動で定義されるようになっています。この機能のおかげで、そのクラスのモックを後から作る際に別途でインターフェースを定義する必要がなくなっています。
また、レイヤー間の依存関係についてもあまり神経質にならなくてもよいという考えに変わりました。多くのモバイルアプリではAPIから取得した値を画面に表示することが主な関心事になりますが、そのような開発において、モバイル側で無理にドメインモデルを作ったとしても、メリットより運用の手間が上回ることのほうが多い気がしています。また、仮に分離が必要になっても、前述したDartの言語使用のおかげでインターフェースへの書き換えはそこまで手間ではありません。
アーキテクチャパターン(状態管理において)
クライアントサイドの領域において、これまで様々なアーキテクチャパターンが考案され、名前がつけられてきました。例えば、MVC(Model View Controller)、MVVM(Model View ViewModel)などがあります。クライアントサイドのアーキテクチャパターンは、モデルの状態をいかにViewに反映するかという課題を解決するために生み出されたものがほとんどです。
私もこれまで様々なアーキテクチャパターンを学び、プロジェクトごとに最適なパターンはどれなのかを考え、試行錯誤しながら開発していました。パターンごとにメリット・デメリットがあるため、適切なアーキテクチャパターンを選定してプロジェクトに実装することはプログラマとして腕の見せ所でもありました。
そして、私がFlutter開発を始めた際にも、アーキテクチャパターンが必要だろうということで、FlutterとRiverpodの機能でどうにかパターンを実装しようと躍起になっていました。また、私の他にも同じようなことを考えている方々がいるらしく、インターネットで「RiverpodでMVVMを実現する」というような内容のQiita記事を見つけて参考にしていました。しかし、開発を続けるにつれ、MVCやMVVMをRiverpodで実装することにはあまりメリットがない、ということがわかってきました。
RiverpodでMVCやMVVMっぽいプログラムを実現することは可能です。しかし、それらのアーキテクチャに従おうとするとRiverpod公式のプラクティスから離れてしまうことがありました。また、「MVC/MVVMと呼べそうなアーキテクチャ」を実現できたとして、Riverpodのベストプラクティスから離れるという欠点を上回るほどのメリットを実感することはできず、無理やり寄せたことによる歪みのほうが目立ってしまいました。
Riverpodを普通に使っていると、部分的にはMVC, MVVMと呼べそうなプログラムになることは多々ありますが、それらのプログラムにパターン名を付けて分類することにもあまりメリットがないように思います。
結果、今の自分の考えとしては、Riverpodの公式ベストプラクティスをそのままアーキテクチャパターンとして受け入れるというところに落ち着いています。それが一番無駄がなく、快適に開発できると感じています。
Riverpodに委ねる
さて、ここまで、3つの気にしなくていいことを紹介しました。
Flutter開発者がこれらのことを気にしなくて済むのは、FlutterSDKとRiverpodのエコシステムによるところが大きいです。特に、Riverpodは状態管理と依存性の注入(DI)を一手に担っているライブラリであり、おかげで開発者は難しいことを考える必要がなくなっています。自分がこれまで経験してきたプラットフォームでは、状態管理用のライブラリとDI用のライブラリは別れていることがほとんどで、プロジェクトの特性に応じて使い分ける必要がありました。しかし、Riverpodはこれらが渾然一体になっており、状態やDIをほとんど意識しなくても問題のないコードが書けるように設計されています。
その反面、渾然一体となっているが故に、Riverpodを部分的に取り入れるというアレンジは難しいように思います。中途半端に他の状態管理手法や設計法を取り入れてしまうと、Riverpodのベストプラクティスから離れてしまうことが多く、デメリットが目立ってきます。Riverpodを使うときは、全てをRiverpodに委ねるつもりで従うほうがよいです。
Riverpodを使ったプロジェクトの設計に関する記事やドキュメントは数多くありますが、個人的には以下の二つのドキュメントに愚直に従えばほとんどのアプリ開発を快適に行えると感じています。
まずはこれらのドキュメントに書かれていることを素直にやってみて、それでも困ることが出てきたらその時に他の考え方を検討するぐらいでちょうどいいかもしれません。
まとめ
この記事では、自分がUnityからFlutterに転向した時の苦労を元に、Flutter+Riverpodでアプリを作る際の注意点を紹介させていただきました。ご参考になれば幸いです。
※本記事の内容はTP個人の意見であり、会社を代表するものではございません。

株式会社SODAの開発組織がお届けするZenn Publicationです。 是非Entrance Bookもご覧ください! → recruit.soda-inc.jp/engineer
Discussion