概要
掲題のとおりです。2019年5月7日のGoogle I/O にて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 の構築と同じなので、ハマりポイント以外割愛します。
そこからという方は、以下がわかりやすかったです。
一点、ゼロから環境を作る際、Android SDK のライセンスが認識されないというのがありましたのでそこだけ解決法を。
※ Windows アプリを作るとしてもそのライセンス問題を解決しないとビルドできません。
[√] 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 以上の場合、こんなエラーが出ちゃいます。
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 インストーラから入れておきます。
なお、 MS Build は 個別のコンポーネント
の箇所にあります。
Flutter の設定
あとは、こちらに書いてる通りにやればできます(多分)
Desktop support for Flutter
https://flutter.dev/desktop
$ flutter channel master
$ flutter upgrade
$ flutter config --enable-windows-desktop
その後、以下のように表示されたら設定はうまくできてます。
$ flutter devices
Windows • Windows • windows-x64 • Microsoft Windows [Version 10.0.18363.836]
作成したプロジェクトへの反映
あとは、作成したプロジェクトのルートディレクトリにて以下のコマンドを打つことでプロジェクトルート直下にWindowsフォルダなどが作成されます。
$ cd <プロジェクトルート>
$ flutter create .
# 色々ダウンロードされ、最後に flutter doctor の内容が表示されればOK
その後、 $ flutter run -d windows
とすることでビルドされて最低限動くようになります。
ハマりポイント: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
だけない…)。
※standard_codec.cc
は<プロジェクトルート>/windows/flutter/ephemeral/cpp_client_wrapper
にあります。 - 以下のように実装する
#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 のものと変わりません。
以下のような感じでできます。
void launch() async {
const platform = const MethodChannel('<チャンネル名>');
await platform.invokeMethod("launch", { "path" : "<起動させたいアプリやファイル>"});
}