FlutterでPluginを駆使して高速に開発する

Flutterでアプリ開発をするにあたり、最近取り組んでいる考えとかやり方を紹介します。

基本的な考え、方針

Flutterの仕組みを色々と考えてみると、既存の様々な開発現場を踏まえた仕組みとなっていることに気づきます。

  1. FlutterはAndroid/iOSのコードを共通化できるが、リポジトリを(必ずしも)共通化する必要はない
  2. FlutterはWidgetが中心になるため、ApplicationとFluginのコードに本質的な違いがない
  3. pubspec.yamlはGitの操作と親和性が高い

これらをベースに、早くて易い開発を考えていきます。

Flutterのコードとアプリのリリース

Flutterは、1つのコードでAndroid/iOS/Webのアプリケーションを作成することができます。 ビルド対象のプラットフォームを指定してコマンドを叩くだけなので、とても簡易です。

flutter.dev

flutter.dev

しかしリリースや運用を考えると、全てのプラットフォームを同一に扱うことが難しいケースが思い浮かびます。 例えば、AndroidiOSではストアの審査がありますが、Webにはありません。このため、市場に展開するまでのリードタイムが異なります。 そのほかでは、iOSのみアプリケーションの修正が求められることもあります。iOSのリリースまでAndroidのリリースを待っても良いですが、別でリリースしたい場合もある時にはあります。

Flutterのアプリを作成するリポジトリが単一であると、上記のような課題に対応するための方法が限られます。Tagをプラットフォームごとに管理したり、Branch戦略を工夫したり、といった手段を取る必要があります。 *1

Flutterを使用してアプリを開発するとき、そもそもの話になりますが、BranchやFlowに苦心するよりも開発に苦心したいものです。 そんなわけで「アプリをリリースするために管理するリポジトリ」と「アプリの機能を管理するリポジトリ」を分割する、そういった対策を提案しています。

Pluginに分割できるものとできないもの

Flutterのコードを眺めていると、大体3つに分類することができます。 この3つの分類は、Applicationとして書いてもPluginとして書いても変わりはありません。

  1. Pure Dartのコード
  2. Flutter Widgetのコード
  3. Flutterから動作しているプラットフォームのAPIをリクエストするコード

Flutterのコードを書いている時、各プラットフォームへの対応を行う処理をPluginに分割しておくと、そのPluginを呼び出すコードはプラットフォームを意識しなくて良くなります。 普段利用している、Firebaseの各ライブラリなんかがいい例です。コード全体を考えるとプラットフォームを意識しなければならない箇所は絶対に出てきますが、そういった箇所はPluginに切り出しておきたい。

Pluginに分割することで、「機能のみのテスト」や「プラットフォームのSDKバージョンの変更テスト」などがしやすくなります。また「Pluginのバージョンを戻す」ことで機能ロールバックがしやすくなります。 なおテストは、ユニットテストが望ましいのですが、利用しているSDKのパターンによっては手動テストも想定しています。DartからAndroidのViewを呼び出すような処理を、どのようにユニットテストすればいいのか悩むぐらいなら、ささっと手動でテストするフローにしたほうが確実です。


PluginはApplicationに依存することができないので、Widget単位でProviderにより依存関係を整理してしまえば、あとは単純なDIをすれば良くなります。 この辺りの実装方針については、別のブログでまとめているので、よければ参考にしてみてください。

tech.studyplus.co.jp

一方で、Pluginに切り出すことができないものがあります。 利用するSDKの初期化処理、URLでアプリを開くための設定のようなプラットフォーム固有の処理などです。 Firebaseのサービスに接続するためのjsonはApplicationで管理しならず、Universal LinkのURL設定はアプリケーションのplistに設定する必要があります。

ざっくりと振り返ってみると、DartとKotlinやSwiftで書かれている部分の大半は、ApplicationとしてもPluginとしても差がありません。 個人的な意見では、どれだけ処理をPluginとして切り出せる状態にできるか考えたほうが、Dartで書くべきコードの範囲が適切に分割されます。

flutter create --template packageflutter create --template plugin で生成されるものは異なりますが、ここではそれらをまとめて「Plugin」と呼んでいます。

Flutterとpubspec.yaml

Flutterが依存関係を解決する仕組みはすごい*2のですが、pubspec.yamlもすごい仕組みです。 これがデフォルトで入っているのは、後発フレームワークだな〜と思います。

flutter.dev

公式ページを確認していただきたいのですが、ライブラリのバージョンを固定したりできるだけでなく、「Git Repositoryを指定して、Pluginを取得する」ことができます。 これはかなり便利です。アクセス可能なGitHubリポジトリであれば、コミットを指定してPluginとして読み込むこともできます。

github.com

上のケースはpub.devで公開されているパッケージが、GitHubで公開されているコードと一致していない状況に対応しています。最新のFlutterでビルドできなくて困ったとき、本当に便利です。 *3


さて、前項でPluginについてあれこれ書きましたが、Privateなアクセス権限のあるPluginを作りたいと思ったことはないでしょうか? つまり閲覧や編集の権限があるユーザーのみ取得できる、そんなPluginを作りたいと思ったことはないでしょうか?

Private Repositoryとpubspec.yamlの組み合わせで、PrivateなPluginを作ることができます。 そしてGitでアクセス可能であれば利用できるので、Private Repositoryを置く場所は任意です。 つまり社内向けのサーバーでもいいですし、GitHubでもGitLabでも構いません。 Private PluginはCommitを指定してもいいですし、Git Tagを利用することもできます。便利ですね。


制限がないわけではありません。 Gitを利用して依存関係を解決すると、^によるライブラリバージョンの解決がうまくいかない、という問題があります。 この問題に対応する良い方法は現時点であまり思い浮かんでおらず、coreなライブラリをまとめるPluginを作るかなぁ、ぐらいの感じです。

また、Private RepositoryのアクセスはCIから実行するのが骨だったりします。 大抵の場合、Deploy Keyによるsshアクセスをすれば解決したりするのですが、少しだけ利用しているCIツールの知見が問われるます。


現状、GitHub PackagesがDartのライブラリに対応していないので、Private Repositoryを紹介している面があります。 GitHub PackagesがDartに対応してくれれば、だいぶ話をしやすくなるので、期待ですね。

github.com

開発のスピードを保つ

開発を高速にしたい、Flutterを使っているのであれば尚更そのような気持ちになります。

開発のスピードは「コードのサイズ」が膨らむと維持するのが難しくなってしまいます。 既存のコードが大きくなればなるほど、速度は工夫なしには上がりにくくなるものです。 特にUI開発において、複数の箇所から呼び出されるコードを書く際には、適切な抽象化や関心の分離が欠かせません。


2021年時点では、Android推奨のアーキテクチャとして明言はされていないものの、マルチモジュール アプリで Dagger を使用するとして紹介されています。 当初は並列ビルドによるビルドの高速化が見込まれていたものの、設計を助ける意味でマルチモジュール構成は非常に高い効果を発揮しました。

マルチモジュール構成にすることで得られる、設計上のメリットは色々とありますが、ここで強調したいのは下記2つです。

  1. モジュール間の依存関係が整理される
  2. モジュールごとの開発に区切られる

Flutterでモジュール分割を行う場合、単一のリポジトリで複数のモジュールを構成する方法と、複数のPluginにより構成する方法があります。 ありますが、前述の理由の通り複数のPluginにより構成する方法をお勧めします。

Flutter Plugin(Package)

Flutter Plugin(Package)の開発を始めるのは非常に簡単です。 *4 簡単なので、できれば手元でターミナルで実行しながら読み進めてみてください。

flutter.dev

Pluginを作成するコマンドを叩くと(バージョンによって変わる可能性があるので、コマンド自体はドキュメントで確認してください)、ものの数秒でPluginのテンプレートが生成されます。 この記事を書いている2021年10月では、下記のような構成でファイルが生成されます。

lib/
pubspec.yaml
README.md

このままだとPluginの動作確認がしにくいので、さくっとPluginを実行できるプロジェクトを追加します。 慣例的にexampleディレクトリを追加し、exampleディレクトリ内でFlutterのApplication作成してみてください。

example/android/
       /ios/
       /web/
       /lib/
       /pubspec.yaml
       /README.md
lib/
pubspec.yaml
README.md

あとはexample/pubspec.yamlから相対パスを利用し、rootにあるPluginを読み込んであげれば準備は完了です。複数のPluginを構成する場合はoverrideを駆使したり、複数の実行プロジェクトを作りたい場合は、実行用プロジェクトを任意の数増やしてみてください。

今回紹介した構成は、一般的なFlutter Pluginの構造になります。 もしもイメージしにくければ何らかのPluginを見てみてください。 参考例として、個人で作っているPluginのリンクを貼っておきます。

github.com

なおflutter_auth_uiの場合は、Pluginが各プラットフォームのSDKを呼び出す都合上、Plugin側にandroidiosディレクトリがあります。


exampleディレクトリの中で作成するアプリは、テストのためのアプリであるので、必要最低限のコードがあれば十分です。 また構造としては、通常のFlutter Applicationであるので、どのようなPluginでも追加できます。

もしもPluginからApplicationへ何らかのデータを返す場合には、Pluginの一番外側にCallbackメソッドを仕込んでおくことになります。 この時、公開するべきメソッドやクラスをexportを利用して制限することで、Pluginの中に(ある程度)処理を閉じることもできるのでご検討ください。

外部からFirebaseInstanceを引き受ける形などにしておけば、Plugin内でアクセスするオブジェクトをPluginの外から操作できます。 Plugin単位でProvideする処理などを書いてあげれば、適切に状態を管理もできます。アプリ全体の管理をするより、コードのサイズが小さくなる分、シンプルに考えて実装していきましょう。

Pluginによる開発の活用

段階的にFlutterへの移行を計画している場合、機能をPluginとして開発すると色々と嬉しいことがあります。 特に課題の多い、しかし検討にあがりやすいAdd-to-Appを例にすると下記の通りです。

  1. Flutter Pluginとして動作テストができるので、Add-to-Appの問題とFlutterの問題を切り分けられる
  2. Add-to-Appの本番リリースの前に、Flutter Pluginとして機能の開発とテストが開始できる
  3. Add-to-Appで搭載したい機能を、Git TagやGit Branchの指定で切り替えられる

flutter.dev

また、「将来的にFlutterで作ったアプリにリプレースする」ことを検討している場合、Pluginとして作成したコードはそのまま活用できます。 前述の通り、Pluginとして「複数のリポジトリから参照されることを前提にして」開発しておくと、リプレースやリリースのスケジュールと整合性も取りやすいはずです。


ほか、Pluginによる開発は「開発規模を小さく保つ」ことができるため、新規にFlutterアプリケーションを開発するメンバーにも向いています。 場合によっては、AndroidiOS、WebのAPIとの接続だけに集中して開発できることもあり、もともと持っている知見を十二分に活用して開発ができるケースもあります。

まだ知見を溜めきれていませんが、Pluginを実行するアプリケーションレベルで社内配布を行うこともできます。 その辺りも、新規にFlutterプロジェクトに参加するエンジニアが力を発揮できる箇所になるでしょう。

まとめ

まとめです。 本記事で強調したかったのは、下記3点です。

  • Flutterの開発でもマルチモジュールな開発を取り入れることができる
  • Git Repositoryを複数作り、それぞれをFlutter Pluginとして小さく検証しながら開発できる
  • Flutterのアプリを「FlutterのPluginをまとめた存在」として定義し、リリースフローとの整合性を取る層と見なすことができる

なお、Git RepositoryはGitHub Packagesのような仕組みがあれば、そちらに置き換えることができます。 開発が頻繁な時にはBranch指定を行い、安定してきたらGitHub Packagesから参照するなどできると、非常に開発が楽しくなるのではないかなと思っています。


Flutterを利用した最高のDXを探していきましょう!

*1:もちろん、リリースを完全に揃えることで単一リポジトリで管理する方法もあります。制約を加えることで、シンプルで高速なリリースは可能です。

*2:PubGrubをご確認ください。

*3:ワークアラウンドはつらい。

*4:Pluginの場合、個人的な意見により、Javaを選択することをお勧めしています。