はじめに
この記事はカービューでアプリ開発をしている@masa24と@nbappsが既存のAndroidとiOSアプリにFlutterを導入した感想です。
Flutterの記事自体はいくつかあるものの、Add-to-appを利用して既存アプリに導入した事例は少なかったため、誰かの参考になればと思い本記事を執筆しました。
導入してみて良いと思った点
両OS対応
**Flutterを導入する最大のメリットです。**ネイティブで両OS対応しようとするとObjective-C、Swift、Java、Kotlinと4つもの言語の知識が必要になりますが、FlutterならDartの知識だけで済みます。
しかも標準のWidget(画面のコンポーネントです、AndroidのTextViewやiOSのUITextViewを思い浮かべてください)に沿って実装するだけでMaterial Designに沿った美しいデザインのアプリが完成します。
書きやすい・コードの量が少ない
Flutterはモバイルアプリ用フレームワークだからか、アプリによくある処理が非常に書きやすいです。
例えば画像を取得する処理は以下のように1行で書くことが出来ます。
// ネットワークから取得した画像を表示
Image.network("画像のURL")
// アプリ内のリソースを利用して画像を表示
Image.asset("画像のパス")
非同期処理もお手の物です。
// 非同期処理
Future<String> getString() {
return Future.delayed(Duration(seconds: 1), () => "test");
}
// 非同期処理を受け取って何かを実行
void test() {
getString().then((value){
// valueを受け取って何かできる
});
}
Android Studioで利用できるのも◎です。何かと悩まされるXcodeを使わなくて済みます!
更に、ネイティブで画面を作成する場合はXMLやxibでレイアウトを作成して、対応するViewとViewModelを作成して…と、1画面作るだけで結構な量のコードが必要になりますが、Flutterならコードがレイアウトを表してくれるためかさばりません。
Viewとレイアウトの繋ぎこみが不要なのも魅力です!
ビルドが爆速
FlutterにはHot Reloadという機能があります。(Hot Reload自体については公式ドキュメントを参考にしてください)
サブモジュールとして作ったFlutterプロジェクトをAndroid Studioで開くだけで利用可能になり、2回目のビルド以降は一瞬で完了するというスグレモノです。
これを使うことでFlutterプロジェクトだけを爆速で完成させることができます。あとはネイティブに組み込むだけです!
Hot Reloadについての公式ドキュメント:https://flutter.dev/docs/development/tools/hot-reload
つまづいた点
CachedEngineを使わないと画面遷移が遅い
Flutterで作成した画面を呼び出す方法はいくつかあります。ViewやFragmentとして呼び出す方法もありますが、Activityとして呼び出す方法をここでは紹介します。
// FlutterEngineを使わない場合
FlutterActivity.createDefaultIntent(this)
// FlutterEngineを使わない場合2
FlutterActivity
.withNewEngine()
.initialRoute("/my_route")
.build(this)
// CachedEngineを使う場合
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
3つの呼び出し方がありますね。上記のうち上2つの呼び出し方でFlutterの画面を呼び出した場合、画面が表示されるまでに数秒かかってしまいます。
プロダクトとして利用する場合はCachedEngineが必須なレベルです。
CachedEngineを使って複数の画面を作った場合を実質サポートしていない
FlutterはWebを意識しており、routeという概念を使って遷移させます(URLみたいなものです)。
CachedEngineを使って複数の画面に遷移させようとすると、このrouteを指定するプロパティがありません。
この問題について、公式には「This is because a cached engine is expected to already be running Dart code, which means it’s too late to configure the initial route.」と書いてあります。
CachedEngineを使った時点でDartのコードが実行されるため、初期ルートを設定するには遅いからだそうです。
MethodChannelを活用すればなんとかなるのですが、いろいろ工夫が必要なため公式のサポートを待ちたいところです。
MethodChannelを活用する例:
void main() => runApp(testPage())
class testPage extends StatelessWidget{
// 省略
}
class MyWidget extends StatefulWidget{
@override
_MyWidget createState() => _MyWidget();
}
class _MyWidget extends State<MyWidget> with WidgetsBindingObserver{
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state == AppLifecycleState.resumed) {
MethodChannel("methodchannel").setMethodCallHandler((MethodCall call) async {
switch(call.method) {
case "test":
return Navigator.pushAndRemoveUntil(context,
new MaterialPageRoute(builder: (context) => Route.getWidget(call.arguments)), (route => route.isFirst);
}
}
}
}
}
class Route{
static Widget getWidget(String route) {
switch(route) {
case 'route1':
return AnotherPage();
}
}
}
Intent intent = FlutterActivity
.withCachedEngine("cachedengine")
.build(context);
MethodChannel methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "methodchannel");
methodChannel.setMethodCallHandler((call, result) -> {
// 何らかの処理
});
methodChannel.invokeMethod("test", "route1");
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let flutterViewController = FlutterViewController(engine: vcDelegate.flutterEngine, nibName: nil, bundle: nil)
let methodChannel = FlutterMethodChannel(name: "methodchannel", binaryMessenger: methodChannel.setMethodCallHandler {
// 何らかの処理
}
viewControllerDelegate?.navigationController?.pushViewController(flutterViewController, animated: true)
methodChannel.invokeMethod("test", arguments:"route1")
ネストしがち
dartはとにかくネストします。百聞は一見に如かず、Flutter公式のサンプルアプリのコードを見てください。
92行目以降のネストにびっくりすると思います(Lisp経験者ならそうでもない?)
https://github.com/flutter/samples/blob/master/add_to_app/flutter_module/lib/main.dart
もっとも、これは慣れの問題であり、
・適宜関数として切り出せばよい
・Android Studioでは対応してるカッコを明示してくれる
ので書いているうちに気にならなくなるとは思います。
ビルドしないとレイアウトが見れない
画面のレイアウトを作る際、Androidではxml、iOSではxibで見ながら設計できますが、Flutterはビルドしないと見れません。
Hot Reloadが爆速なのですぐに気にならなくなると思いますが、最初はとっつきにくいと思います。
ネイティブ側とFlutter側で同じような変数・関数・リソースが必要
Flutter側とネイティブ側で処理を共有したりはできないので、変数・関数・リソースの重複が起きます。
例えば、HTTPメソッド(GET・POST・DELETEなど)は通信処理が必要なアプリではどうしても書く必要があります。
変数も、Flutterからネイティブ・もしくはネイティブからFlutterの処理を呼び出すMethodChannelという機能を使うときなどに重複しがちです。
/// Flutterからネイティブの処理を呼び出す。
/// ここだけでも"methodchannel"と"test"の定数をFlutterとネイティブの両方で宣言する必要が…
void test() {
MethodChannel("methodchannel").invokeMethod("test", null);
}
MethodChannel methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "methodchannel")
methodChannel.setMethodCallHandler((call, result) -> {
if(call.method == "test") {
// 何らかの処理
}
});
リソース(画像など)も同じものが必要です。(これはもしかしたら回避策があるかもしれません。)
まとめ
以上、App-to-appを利用して良いところとつまづいたところを紹介しました。
まだまだ課題はたくさんありますが、実際に書いてみると可能性を感じるフレームワークです。
StackOverflowの調査記事( https://insights.stackoverflow.com/survey/2020 )でもMost Loved Frameworksとして第3位にランクインしています。
既存のアプリへの今すぐの導入はおすすめしませんが、新規でアプリを作る際にはおすすめです。今後とも注目ですね。
カービューではFlutterの導入のような技術的チャレンジを歓迎しています。応募をお待ちしております!
採用ページ:https://www.carview.co.jp/recruit/