Flutterが銀の弾丸だったらいいな
最近弊社では、 #HASH というWebサービスが、ローンチされたのかされてないのか微妙な状態でなし崩し的にベータ版みたいな感じで公開されております。
「ハッシュタグでのゆるいつながり」をテーマにしたサービスで、ハッシュタグの作成や管理も簡単にできるようになっていますのでよろしければ使ってみて下さい。
で、まだベータ版みたいな状態ではあるんですがやっぱりこういうのはアプリも欲しいよね!ということでアプリ化の検討を始めました。そして検討する中でiOSとAndroid両方で動かすことが出来るいわゆるクロスプラットフォームな開発ツールの導入を検討してはどうか、という話になり、最近活発に開発が進んでいて(一部で)話題になっているFlutterというSDKを検討してみることになりました。
Flutterとは
Flutterは上述のとおり、iOSとAndroid両方で動作するアプリを開発することが出来るクロスプラットフォーム開発用のSDKです。Googleがオープンソースで開発していて非常に活発に開発が行われています。
こういうクロスプラットフォーム開発SDKにはいくつかのパターンがあるのですが、Flutterは
- 独自のUIコンポーネントを使って描画を行い
- 独自のAPIセットが提供されているためプラットフォーム間でコードが共有出来る
というものです。
ちなみに有名どころのクロスプラットフォームSDKであるXamarinは、
- ネイティブのUIコンポーネントを利用してUIを構築し
- 各プラットフォームAPIに対応する薄いラッパーAPI、もしくは独自のAPIセットが提供、のどちらかを選択できる
といった感じになっています。
検証したこと
今回はあくまで業務で使えるのか、そしてネイティブ実装で作るより明確なメリットがあるのか、ということが検証の主眼ですので、あまり技術的なことを細かくは書きません。(もちろんすべて実際にコードを書いて検証は行っています)
結局知りたいことって、
- どこまでコード・UI設計が共通化出来るのか
- 各プラットフォーム固有の機能がどこまでサポートされているのか
- 各プラットフォームネイティブの環境でそれぞれのアプリを作ったときと比較してどの程度工期が短縮されるのか
っていうことなんですよね。
ではまず、
<どこまでUI設計が共通化出来るのか>
ということですが、UIについてはほぼ共通化が可能です。ただし今のところビジュアル設計ツールはなくコードでUIを記述していく形になります。
ただ、一度ビルドしてテスト端末などでデバッグ実行している状態でコードを変更すると、再ビルドすることなく実行した状態のままコードが反映されるというHot Reloadという機能があるため、意外と面倒には感じないのではないかと思います。
ただし、例えば広告を掲載したい、とかいうことになると現状admobしか対応していない、とか、アニメGIFなどネイティブ実装ではオープンソースのライブラリを使って簡単に表示できたりするのですが、そういったライブラリがまだあまり種類がなく、自力での開発が必要となる場面が多くなるような気がします。
<どこまでコードが共通化出来るのか>
コードについては、Flutterの標準ライブラリが対応している範囲であれば同一コードで記述できますが、対応していないものについてはChannelという仕組みを使って、ネイティブコードを呼び出す形で実装することになり、この場合swiftやkotlinなどでネイティブ実装する必要が出てきます。
ここは結構キモだと個人的に思うところですので、前提知識なくコードを見てもよくわからないかもしれませんが、ここだけは実装コードサンプルを載せてみます。
下記でやっていることは「ネイティブ実装を呼び出して文字列を取得する」という単純なもので、雰囲気だけ伝われば幸いです。
まずDart側でこのようなメソッドを準備します。ネイティブ側実装を呼び出す際にawaitでブロッキングしますのでこういうメソッド(非同期呼び出し)が必要になります。
Future<Null> _getCallbackDataString() async { String dataString; try { dataString = await platform.invokeMethod('iDataIO_getCallbackData'); } on PlatformException catch (e) { dataString = e.message; } setState(() { _callbackDataString = dataString; }); }
この中の
platform.invokeMethod(‘iDataIO_getCallbackData’);
これがmethod channel というネイティブ側実装を呼び出すコードです。
そしてiOSの場合は、Flutterの動作ベースとなるAppDelegateのapplicationメソッド内に下記のようなコードを記述します。
let dataIOChannel = FlutterMethodChannel.init(name: "net.bugfix.io/dataio", binaryMessenger: controller) dataIOChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch (call.method) { case "iDataIO_getCallbackData": result(dataString) default: result(FlutterMethodNotImplemented); } });
この中の dataIOChannel が上記Dart側のplatform.invokeMethodで呼ばれるオブジェクトとなります。
そして渡されてきたメソッド名によってコードが実行され、resultで実行結果が返却されます。
Androidのコードは省略しますが、MainActivityの初期化コードの何処かに同じようなコードを記述することになります。
確かに工夫すれば結構なんでもやれちゃいそうではあるんですが、今回はここが一番のネックと感じました。
だってこれは完全にネイティブ実装、これなら全部ネイティブで書いたほうがストレスないと思うんですよね(個人的な見解)。。。
<プラットフォーム固有の機能がどこまでサポートされるか>
これも基本的にはコード共有と同じなんですが、例えばiOSの共有メニューに自分のアプリを選択肢として表示させてアクション出来るようにしたい、などについてはその部分はまるっとxCode使ってswiftで記述・plistを編集する必要があります。これも完全にネイティブ実装です。
<工期について>
これはもうネイティブ実装がどの程度必要になってくるか次第ですが、もしネイティブ実装の必要がほぼない、ということであれば、iOS、Androidそれぞれネイティブ実装したときが2だとすると、1.6程度かなといったところです。(がっつり作ったわけではありませんのであくまで私の感覚的なものです)
ただちょっと触ってみた感覚では、結局かなりネイティブコードを書くことになってしまうのではないか、という印象を持ちましたので、きちんとしたものを作ろうと思うほどにネイティブ実装より時間がかかってしまうように感じました。
例えば、オリジナルのソフトウェアキーボードを実装する、とか、上述した他のアプリとデータ共有する(iOSだと共有メニュー, AndroidだとIntent共有)など。
また、プッシュ通知自体はサーバサイドにFirebaseを利用すれば割合簡単に実装可能ですが、これも例えばiOSのプッシュ通知の際の細かな挙動をネイティブと同じように実装するのは、ネイティブ実装呼び出しを利用してもなかなか難しい感じです。
結論
今回、Flutterで実装することは見送る方向で検討しています。
やはりネイティブ実装が必要となる可能性が高く、それなら最初からネイティブ実装したほうが効率が良いのではないか、ネイティブ実装なら簡単に出来ることが予想外に時間がかかってしまう、という可能性がありそうだな、と感じたことです。
あと弊社、一応iOSもAndroidも両方対応可能なエンジニアが私含めていますので生産性という点でもデメリットを上回るメリットがないように感じました。
ただFlutterはまだベータ版で活発に開発が行われているようですので、今回私がネガティブに感じた部分がどんどん改善されていく可能性があります。
Flutterにかぎらずこういうものは時折チェックして乗り遅れないようにしていきたいですね。
開発現場からは以上となります。