既存のアプリからFlutterを呼び出す

この記事は[Flutter Advent Calendar 2018](https://qiita.com/advent-calendar/2018/flutter)の21日目の記事になります。

## はじめに

既に存在しているアプリにFlutterを追加して、一部の画面をFlutterモジュールとして開発したい方向けの記事になります。

公式ドキュメントの[Add Flutter to existing apps](https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps)から抜粋している箇所も多いので、詳しい内容はそちらを見て頂くのがいいと思います。また、今回紹介する[Add2App project](https://github.com/flutter/flutter/projects/28)はまだプレビューの段階なので、今後さらに変更があるかもしれません。

それではさっそくセットアップしていきます！

## セットアップ

### Flutterモジュールを作成する

1. まずはflutterコマンドを使えるように、下記のリンクからflutterをインストールします

    [Install - Flutter](https://flutter.io/docs/get-started/install)

2. 次に既存のアプリから呼び出すFlutterモジュール(flutter_sample_app)を作成します

    ```shell
    $ cd some/path/
    $ flutter create -t module flutter_sample_app
    ```

これでFlutterモジュールの作成ができたので、次に各プラットフォームでモジュールを読み込んで呼び出してみます。

### iOSアプリからFlutterを呼び出す

1. iOSアプリのPodfileにFlutterの設定スクリプトを実行する行を追加します

    もし、Podfileが存在しない場合は `pod init` を実行してPodfileを作りましょう。

    ```shell
flutter_application_path = 'path/to/flutter_sample_app/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
```

2. `pod install` を実行すると、iOSアプリにFlutterを利用するための設定が追加されます

    ```shell
$ pod install
```

3. iOSアプリのTARGETSのBuild PhaseにDartをビルドするためのスクリプトを追加します

    赤丸の+を押すと、ビルド時に実行するスクリプトを追加できるので、下記のコマンドを追加して `Target Dependencies` の後ろに移動させます。

    ```shell
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
```

    <img width="1046" alt="スクリーンショット 2018-12-21 1.43.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/9950/de748e34-abc8-2b88-d9cf-3ddec4f0a553.png">

    これでFlutterをiOSアプリから利用するための設定終わりました。次に実際にiOSアプリからFlutterの画面を表示させてみます。

4. `AppDelegate` の親クラスを `FlutterAppDelegate` に置き替えます

    ```swift
    import UIKit
    import Flutter

    @UIApplicationMain
    class AppDelegate: FlutterAppDelegate {
    }
    ```

5. 次のようにiOSアプリからFlutterViewControllerを利用することで、Flutterの画面を表示させることができます

    ```swift
    let flutterViewController = FlutterViewController()
    self.present(flutterViewController, animated: false, completion: nil)
    ```

### AndroidアプリからFlutterを呼び出す

次に同じFlutterモジュールをAndroidに読み込ませて、Flutterの画面を表示させてみます。

1. Androidアプリにある `settings.gradle` に、サブプロジェクトとしてFlutterモジュールを追加します

    ```groovy
    // MyApp/settings.gradle
    include ':app'
    setBinding(new Binding([gradle: this]))
    evaluate(new File( 
      settingsDir.parentFile,
      'path/to/flutter_sample_app/.android/include_flutter.groovy'                        
    ))
    ```

2. `build.gradle` にAndroidアプリによるFlutterモジュールへの依存関係を追加します

    ```groovy
    // MyApp/app/build.gradle
    dependencies {
        implementation project(':flutter')
    }
    ```

    AndroidアプリからFlutterモジュールを使うための準備が整いました。

3. FlutterモジュールのFlutterクラスからFragmentを受け取って、Androidアプリで表示してみます


    ```kotlin
    val transaction = supportFragmentManager.beginTransaction()
    transaction.replace(R.id.container, Flutter.createFragment(""))
    transaction.commit()
    ```

    Flutterの画面が表示されていると思います。FlutterクラスからはFragmentの他にも、Viewとして受け取ることもできます。

これでiOS/AndroidアプリからFlutterを呼び出して表示することができました！
あとはFlutterモジュールを編集することで、クロスプラットフォームでアプリを開発していくことができます。

## その他

### Hot Reloadを使う

1. iOS or Androidアプリを起動します
2. TerminalでFlutterモジュールに移動して `flutter attach` コマンドを実行します

    ```shell
    $ cd some/path/flutter_sample_app
    $ flutter attach
    Waiting for a connection from Flutter on Nexus 5X...
    ```

3. iOS or AndroidアプリでFlutterの画面に遷移すると、Terminalが下記のようになります

    ```shell
    Done.
    Syncing files to device Nexus 5X...                          5.1s

    🔥  To hot reload changes while running, press "r". To hot 
    restart (and rebuild state), press "R".
    An Observatory debugger and profiler on Nexus 5X is available at: 
    http://127.0.0.1:59556/
    For a more detailed help message, press "h". To quit, press "q".
    ```

4. あとはFlutterのコードを変更して、 `r` を押せばreloadされて再描画されます。

![68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f4c77446b304655585a6864736c45514e54532f67697068792e676966.gif](https://qiita-image-store.s3.amazonaws.com/0/9950/6d1a1106-cec3-928c-986a-301e7d102bc4.gif)

### LaunchScreenを表示させない

FlutterViewControllerは画面を表示する前に、アプリのLaunchScreen.storyboardを表示するようになっています。それを表示させたくない場合、下記のようにsplashScreenViewにViewを設定することで変更することができます。

```swift
let flutterViewController = FlutterViewController()
flutterViewController.splashScreenView = UIView()
self.present(flutterViewController, animated: true, completion: nil)
```

## 参考

- [Add Flutter to existing apps](https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps)
- [FlutterViewController should not show the Launch storyboard in add2app scenarios](https://github.com/flutter/flutter/issues/24420)

## ソースコード

- https://github.com/t-kashima/flutter_sample_app_ios
- https://github.com/t-kashima/flutter_sample_app_android
- https://github.com/t-kashima/flutter_sample_app