FlutterでWindowsデスクトップアプリのネイティブ連携をする

# 概要
掲題のとおりです。2019年5月7日の[Google I/O](https://developers.googleblog.com/2019/05/Flutter-io19.html) にてFlutterのデスクトップアプリ対応が発表され、すでに開発はできるようになっているのですが、まだまだ全然資料がないのでここで環境構築しつつネイティブのAPIを呼び出すところまでを記載します。

## 動作環境
* Windows 10
* Flutter 1.19.0-2.0.pre.211
* Dart 2.9.0
* Java 11 (ただしJava8も使う)
* Visual Studio 2019 Community
* Android Studio 4.0

# Flutterの開発環境インストール (通常版)
最初の部分はiOS / Android の構築と同じなので、ハマりポイント以外割愛します。
そこからという方は、以下がわかりやすかったです。

[Windowsでflutterインストール](https://qiita.com/nkmk1215/items/2b741157bc253350ff18)

一点、ゼロから環境を作る際、Android SDK のライセンスが認識されないというのがありましたのでそこだけ解決法を。
※ Windows アプリを作るとしてもそのライセンス問題を解決しないとビルドできません。

```bash
[√] Flutter (Channel master, 1.19.0-2.0.pre.211, on Microsoft Windows [Version 10.0.18363.836], locale ja-JP)
// 以下で Android license status unknown. と出ちゃう場合
[√] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.6.0)
[√] Android Studio (version 4.0)
[√] Connected device (2 available)
```
ライセンスを承認するには、単純に `$ flutter doctor --android-licenses` とやればいいのですが、環境が Java9 以上の場合、こんなエラーが出ちゃいます。

```bash
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
⋮
```

これは、Androidの `sdkmanager` が Java8 に依存しちゃってるからっぽいです。
そんな時には Java8 をダウンロードしてきて、 環境変数の `JAVA_HOME` にJava8のルートを設定してあげることでうまくいきます。
※ `sdkmanager.bat` が `JAVA_HOME` を参照してるので、単純にPATHに入れても引き続き同じエラーがでます（ここで暫くハマった…)

## Windows デスクトップアプリを作れるようにする
そんなわけで、ここからFlutterでWindowsデスクトップアプリを作れるようにします。
既にFlutterのプロジェクトを作成済で、Androidでは動作確認ができている＆Visual Studioは入れてあるのを前提とします。

### Visual Studio の必須コンポーネントのインストール
Flutter for Desktop (Windows) は、以下のコンポーネントに依存しています。
※ Visual Studio 2019の場合

* C++ によるデスクトップ開発
    * MSBuild
    * MSVC v142 - VS 2019 C++ x64/x86 ビルドツール
    * Windows 10 SDK (最新)

それらを、Visual Studio インストーラから入れておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/6015d3b7-9381-cc20-092f-59810b70332b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/6bc733e3-2291-5ec8-ade7-43c0fa0e9c66.png)

なお、 MS Build は `個別のコンポーネント` の箇所にあります。

### Flutter の設定
あとは、こちらに書いてる通りにやればできます（多分）

Desktop support for Flutter
https://flutter.dev/desktop

```bash
$ flutter channel master
$ flutter upgrade
$ flutter config --enable-windows-desktop
```
その後、以下のように表示されたら設定はうまくできてます。

```bash
$ flutter devices
Windows  • Windows          • windows-x64   • Microsoft Windows [Version 10.0.18363.836]
```

### 作成したプロジェクトへの反映
あとは、作成したプロジェクトのルートディレクトリにて以下のコマンドを打つことでプロジェクトルート直下にWindowsフォルダなどが作成されます。

```bash
$ cd <プロジェクトルート>
$ flutter create .
# 色々ダウンロードされ、最後に flutter doctor の内容が表示されればOK
```

その後、 `$ flutter run -d windows` とすることでビルドされて最低限動くようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/6d06f70a-6221-517d-a8c0-6d050a11b64a.png)


#### ハマりポイント：Run時に「Error: クライアントは要求された特権を保有していません。 」と出る
コマンドプロンプトから実行した場合、「Error: クライアントは要求された特権を保有していません。 」と出ることがあります。
その場合、管理者権限でコマンドプロンプトを実行するか、Android Studio のターミナルから実行することで回避ができます。

## ネイティブ連携する
ここからが本番です。
まだ Flutter for Desktop はできたばかりっぽいので、ほとんど公開されているプラグインがありません。なので、ネイティブの実行などをやりたかったりすると自作しないといけなかったりするかと思います。
なのでひとまず、**Windowsの外部アプリを起動する** というものを作ってみようかと思います。

ちなみに、以下のデスクトップ用のプラグインもありました。

Desktop Embedding for Flutter
https://github.com/google/flutter-desktop-embedding

### Windows側の実装
結論を書くと、以下の感じで実装できます。

* `<ルートディレクトリ>/windows/Runner.sln` を開く
* ソリューションエクスプローラにある `Runner/Source Files/Client Wrappper` に `standard_codec.cc` を追加する  
※ 以下がないとネイティブ連携を実装してもビルドが通りません(そしてなぜかデフォルトでは `standard_codec.cc` だけない…)。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/06a1aee6-b350-7400-c89c-f19ad82f26a3.png)  
※ `standard_codec.cc` は `<プロジェクトルート>/windows/flutter/ephemeral/cpp_client_wrapper` にあります。  
* 以下のように実装する

```c++:とりあえずflutter_windows.cpp内
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <windows.h>
// 省略

void FlutterWindow::OnCreate() {
   // 省略
   RegisterPlugins(flutter_controller_.get());
   // ここから
   auto reg = flutter_controller_->GetRegistrarForPlugin("<適当なかぶらない名前>");
   flutter::PluginRegistrarWindows* manager = flutter::PluginRegistrarManager::GetInstance()->GetRegistrar<flutter::PluginRegistrarWindows>(reg);
   auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
                     manager->messenger(), "チャンネル名", 
                     &flutter::StandardMethodCodec::GetInstance()
                  );

   // ネイティブ連携のハンドラーメソッドを登録する
   channel->SetMethodCallHandler([](const flutter::MethodCall<EncodableValue>& method_call,
                                    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {
      if (method_call.method_name().compare("launch") == 0) {
          std::string path;
          if (method_call.arguments() && method_call.arguments()->IsMap()) {
              const EncodableMap& arguments = method_call.arguments()->MapValue();
              auto path_it = arguments.find(EncodableValue("path"));
              if (path_it != arguments.end()) {
                  path = path_it->second.StringValue();
              }
          }
          if (path.empty()) {
              result->Error("argument_error", "No Path provided");
              return;
          }

          size_t size = path.length() + 1;
          std::wstring wpath;
          wpath.reserve(size);
          size_t outSize;
          mbstowcs_s(&outSize, &wpath[0], size, path.c_str(), size - 1);
          // 外部アプリの実行
          ShellExecute(0, 0, wpath.c_str(), 0, 0, SW_SHOWDEFAULT);

          result->Success();
       } else {
          result->NotImplemented();
       }
  });
   // 以下略
}
```

### Flutter側の実装
こちらは、iOS / Android のものと変わりません。
以下のような感じでできます。

```dart
  void launch() async {
    const platform = const MethodChannel('<チャンネル名>');
    await platform.invokeMethod("launch", { "path" : "<起動させたいアプリやファイル>"});
  }
```