Add-to-appを使って既存プロダクトにFlutterを導入した話

## はじめに
この記事はカービューでアプリ開発をしている@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行で書くことが出来ます。**

```dart:image.dart
// ネットワークから取得した画像を表示
Image.network("画像のURL")
// アプリ内のリソースを利用して画像を表示
Image.asset("画像のパス")
```

非同期処理もお手の物です。

```dart:async.dart
// 非同期処理
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として呼び出す方法をここでは紹介します。

```kotlin:test.kt
// 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のコードが実行されるため、初期ルートを設定するには遅いからだそうです。

引用元：https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen?tab=custom-activity-launch-kotlin-tab#initial-route-with-a-cached-engine

MethodChannelを活用すればなんとかなるのですが、いろいろ工夫が必要なため公式のサポートを待ちたいところです。

MethodChannelを活用する例：

```dart:main.dart
void main() => runApp(testPage())
```


```dart:TestPage.dart
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);
                }
            }
        }
    }
}
```

```dart:Route.dart
class Route{
    static Widget getWidget(String route) {
        switch(route) {
            case 'route1':
                return AnotherPage();
        }
    }
}
```

```java:test.java
Intent intent = FlutterActivity
        .withCachedEngine("cachedengine")
        .build(context);

MethodChannel methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "methodchannel");
methodChannel.setMethodCallHandler((call, result) -> {
    // 何らかの処理
});
methodChannel.invokeMethod("test", "route1");
```

```test.swift
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**という機能を使うときなどに重複しがちです。

```dart:methodchannel.dart
/// Flutterからネイティブの処理を呼び出す。
/// ここだけでも"methodchannel"と"test"の定数をFlutterとネイティブの両方で宣言する必要が…
void test() {
    MethodChannel("methodchannel").invokeMethod("test", null);
}
```

```kotlin:methodchannel.kt
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/