Flutterでヘルスケアサービスを作る中で意識しているネイティブプラグインの扱いの話
こんにちは、FiNC TechnologiesのiOSチーム西村(@taktem_com)です。
FiNCと言えば、toCのアプリのイメージが強いと思いますが最近は、法人のお客様とのお仕事に関わることも増えました。
基本的には、FiNCで蓄積してきたヘルスケアサービスの知見を活かし、以下にご紹介するような、ライフログに関わるアプリが多いです。
※リリース日順に掲載
A-RROWG(日本電気株式会社様)
Sleep Concierge(帝人フロンティア株式会社様)
また、この他にも、法人のお客様に向けた自社パッケージ製品としての開発も行っています。
上記プロダクトは自分でコードを書いているものも、部分的に関わっているものもありますが、すべてFlutterでの開発を行っていまして、せっかくなので今日は、FiNCでFlutterとどう付き合っていっているかの話をしていこうと思います。
ネイティブコードで外部SDKを利用する
Flutter自体の特徴や基本的なメリットなどは、すでに広く知られているものだと思うので、今回はご紹介したアプリに共通する特徴である、外部ハードウェアとの連携のためのネイティブプラグインの取り扱いについてピックアップしてみます。
とは言っても、基本的にはSwiftで開発する時と、ベースの考え方は変わりません。
外部ハードウェアのために組み込んだSDKの存在を直接的に意識する箇所を適切に隔離することを重要視しています。
ネイティブプラグインの責務は薄く
例えば「外部ハードウェアのLEDを点灯させる」のような、単純なLチカ的なSDKへの命令を、FlutterではなくSwiftで開発する場合のサンプルとして書くとこういうイメージです。
※接続フローなどは考慮しない、発光命令部分のみ
同様の処理をFlutterで扱う場合、 HogeSDKWrapperがネイティブプラグインに対してのラッパに切り替わります。
ネイティブプラグイン側はアプリケーションでの用途を意識したHogeDeviceClientの代わりに、SDKの操作に関するインスタンスの管理と、SDKへの命令を型変換して伝搬することのみに徹することが望ましいです。
この場合でも、Swiftでの開発におけるSDKの取り扱いと観点としては同じで、ネイティブプラグインに対しての処理に必要なインターフェースと、自身のプロダクトで行うハードウェア操作に必要なインターフェースでは、型表現からわけて考えたほうが安全な場合が多いと思っています。
DartとSwiftの関係性をざっくり抜粋して書くとこういうイメージです。
ネイティブプラグインのインターフェースをどう定義するかの観点として、今後Dartに直接対応したSDKが提供された場合、ネイティブプラグインのラッパが、そのままSDKのラッパとして差し替え可能な構造を保つことを一つの基準と考えています。
上記のクラスやインターフェースの関係性を、ざっくり図に起こすとこういうイメージです。
HogeClientは、自分が参照しているのがDartのパッケージなのか、ネイティブプラグインなのかの具体には関心がない状態になっています。
ネイティブプラグインのインターフェース
ネイティブとFlutterは、同じアプリの中に所属するクラスのレイヤー分割というよりは、外部のAPIをコールする場合と同じような基準で定義しています。
例えば、今回利用したSDKの中に、iOSとAndroidでデバイス検索の命令フローが統一されてないものがありました。
具体的には、片方はSDKにscanの命令が直接用意されていたのに対して、もう片方はOS標準のBluetoothの命令を利用して検索する必要がある、というものです。
この場合、ネイティブプラグインのインターフェースとしては、スキャンという目的自体を抽象化して扱い、それぞれのOSでのスキャン命令に必要な具体知識はネイティブ領域に隠蔽するようにしています。
そのため、ネイティブ領域はシンプルなSDKのラッパにとどまらず、具体的な処理命令が入ることになりますが、Dart領域でOS毎に区別して処理を呼び分けるよりは安全と考えています。
ネイティブプラグイン内で外部ライブラリなど利用しない
ネイティブ開発を行っている場合は、外部SDKの状態管理なども、RxSwiftをフル活用していたりするのですが、局所的なプラグイン作成のためにネイティブ領域に外部ライブラリを入れることは原則的には避けています。
- 一部の目的のためにアプリサイズが肥大するのを避ける
- ネイティブ領域は可能な限り薄くするということを基本とした場合、外部ライブラリに頼りたくなった場合は何かしら余計なことをやっている可能性を疑う
というような理由が主で、今の所必要になるシーンには遭遇していませんが、あらゆる場合において絶対に禁止するという強さではないので、状況に合わせて選択していければと思っています。
結果的には
ネイティブ部分、特にiOSはデバッグがめんどうな部分はありますが、可能な限りネイティブ領域を薄く、余計なことを避けるように保つことで、Flutterの恩恵を受けることは出来ていると感じています。
Flutterではパッケージが充実していることもあり、自前でネイティブコードが必要になるシーンは減ってきたとは思いますが、一般的ではないSDKを利用する際や、信頼出来るパッケージが見つからなかった場合などはどうしても避けては通れないので、Flutterを選択したらネイティブ開発知識が不要になるかと言うと、そこまで振り切れるものではないと思いました。
また、外部パッケージの選定をする際にも、ネイティブ開発知識があった方が選択の観点もより適切に行えることもあります。
パッケージによっては、例外の考慮や権限まわりの扱いなどが不適切な形で決め打ちになっていたものなどもあったので、そういう懸念も検知出来たほうが望ましいです。
ネイティブ領域も可能な範囲で学習を進め、その上でFlutterの恩恵を最大化するような工夫ができるようにしていきたいですね。
個人的には、Flutterをきっかけに、Android側のライフサイクルなども勉強になることが多かったので、むしろポジティブにとらえています。
さいごに
実際にFlutterを触ってみて、細かいプラットフォームや言語の違いはあるものの、モバイルアプリケーションの開発を行うにあたって、大事にする点が何かということは基本的には変わらないと感じました
適切にライフサイクルを意識し、適切にクラスを切り分け、人間が継続的に理解できる構造を保つことが大事で、ネイティブ開発で学んできた知見を活かせる部分は実際に多かったです。
そこを大事にした上で
Flutterは、チュートリアルやドキュメントも充実しているし、学習コストは抑えつつクロスプラットフォームの恩恵を効率よく受けられる、素晴らしいフレームワークだと思います。
Swiftの強力な型の恩恵を受けていた身としては言語に対する物足りなさを感じる部分などもありますが、最近ようやくnull safe対応が入ったことなどもあり、まだまだ成長過程の、今後にも期待できる環境だと感じました。
We are hiring!
今回は、ある程度簡易的な例として記載しましたが、一定の規模・複雑性のある開発を行う際に、こういうレイヤー管理の話や、様々な設計における考え方がプロダクトの完成度や、保守性に大きく影響してきます。
中長期的にプロダクトを継続するために、こういった複雑化しないための設計の話で盛り上がりたい、そしてそれらを実行するために一致団結して具体的な解決の結果につなげたい。
というモチベーションを一緒に持てる方、そしてそれを楽しみながらやれそうと思ってくれる方、ぜひお話しましょう。
今回ちょっと固めな内容ではありましたが、普段はカジュアルに仕事の話でも趣味の話でも気軽にできるようなチームで楽しみつつチーム開発を行っています。
気軽にランチにお誘いできるようなご時世ではないですが、オンラインでもカジュアルにお話できると嬉しいです。