Flutterはとても素晴らしいのですが、C#で書けたらもっといいのになと思っていたら、それが実現できる夢のようなツールがリリースされたので試してみました。
Flutnetとは
Flutnetは、XamarinとFlutterを簡単に相互利用できるようにしたフレームワークです。Flutnetを使えば、Flutterで美麗なUIを構築をしつつも、ロジック部分はDartではなく、使い慣れたC#でということが可能になります。有料なのですが、一意のアプリケーションID(バンドルID)しか使えないなどの制限付きのトライアルバージョンがあるので、試す分には無料でできます。
使い方
Flutnetには、GUIツールが用意されていて、簡単にプロジェクトの構築ができます。
インストール
ここからツールをダウンロードできます。使うには、FlutterやAndroidのSDKのパスを通しておく必要があります。
設定例(macOS)
export PATH=$PATH:$HOME/development/flutter/bin:$HOME/Library/Android/sdk/platform-tools
Visual StudioやFlutterなどの環境構築がまだの方は、Windowsの場合は、こちら、macOSの場合は、こちらを参考に行ってください。
注意点
FlutnetはサポートしているFlutterのバージョンが決まっています。作成したプロジェクトに追加されるFlutnet.Interop.Android
、Flutnet.Interop.iOS
パッケージのバージョンがサポートするFlutterのバージョンとなっているので、必ずそのバージョンのFlutterをインストールするようにしてください。(現在の最新(Flutnet 1.0.1 [BETA])では、1.20.2
となります。)
プロジェクトの作成
インストールしたFlutnetのプログラムを起動して、[Next]、[Create]とボタンをクリックしていくとプロジェクトが作成されます。
以下のようなフォルダ構成で作成されます。
MyApp
├── Flutter
│ ├── my_app
│ └── my_app_bridge
├── MyApp.Android
│ ├── Assets
│ ├── MainActivity.cs
│ ├── MainApplication.cs
│ ├── MyApp.Android.csproj
│ ├── Properties
│ └── Resources
├── MyApp.ModuleInterop.Android
│ ├── MyApp.ModuleInterop.Android.csproj
│ ├── Properties
│ └── Transforms
├── MyApp.PluginInterop.iOS
│ ├── ApiDefinitions.cs
│ ├── MyApp.PluginInterop.iOS.csproj
│ ├── Properties
│ └── StructsAndEnums.cs
├── MyApp.ServiceLibrary
│ ├── MyApp.ServiceLibrary.csproj
│ └── Service1.cs
├── MyApp.iOS
│ ├── AppDelegate.cs
│ ├── Assets.xcassets
│ ├── Entitlements.plist
│ ├── Info.plist
│ ├── LaunchScreen.storyboard
│ ├── Main.cs
│ ├── Main.storyboard
│ ├── MyApp.iOS.csproj
│ ├── Properties
│ ├── Resources
│ ├── SceneDelegate.cs
│ ├── ViewController.cs
│ └── ViewController.designer.cs
└── MyApp.sln
Flutter
フォルダ以下がFlutterプロジェクトで、それ以外が、Xamarinのプロジェクトです。MyApp.sln
をVisual Studioで、my_app
をFlutterのプラグインを入れた、Visual Studio Code、もしくは、Android Studioで開きます。my_app_bridge
には、C#のコードから自動生成されるDartのコードが入っています。
ビルド
ビルドは、Xmarinプロジェクト、Flutterプロジェクトの両方で行う必要があります。ひとまず、Visual Studio開いたMyApp.Android
、もしくは、MyApp.iOS
をビルドして、実行してみます。以下のデフォルトのFlutterの画面が表示されたら成功です。もし表示されない様なら、Flutterのバージョンが異なっている可能性があります。
次にFlutterのプロジェクトのビルドを行います。Flutterのコードを変更した場合は都度ビルドが必要になります。ターミナルで、以下のコマンドを実行します。(Visual Studio Codeの場合は、コマンドパレットから、Flutter: Get Packages
でflutterパッケージを予め取得しておきます)
Android
$ flutter build aar --no-profile
iOS
$ flutter build ios-framework --no-profile
さらに、Xamarinに変更を反映させるには、Xamarin側のMyApp.ServiceLibrary
プロジェクトのリビルドが必要になります。加えて、Androidでは、前に入れたアプリの削除もしないと反映が行われないようです。
利用
MyApp.ServiceLibrary
プロジェクトにService1
と言うクラスが作られています。これをFlutterから呼び出すようにします。
[PlatformService]
public class Service1
{
[PlatformOperation]
public string Hello(string name)
{
return $"Hi, {name}! How are you?";
}
}
PlatformService
属性をつけたクラス内の、PlatformOperation
属性をつけたメソッドが、Flutterから呼び出される対象になります。MyApp.ServiceLibrary
プロジェクトをビルドすると、この処理を呼び出すためのDartのクラスが自動作成されます。さらに、呼び出すためには、各プラットフォームでインスタンスの登録も必要です。Androidでは、MainApplication
のOnCreate()
、iOSでは、ViewController
のViewDidLoad()
で行います。
FlutnetRuntime.RegisterPlatformService(new Service1(), "my_service");
登録はプロットフォーム毎なので、MyApp.ServiceLibrary
プロジェクトにはインターフェースのみ追加して、実際の処理は、プロットフォーム毎に書いて登録するということも可能です。
では、Flutterプロジェクトに移り、実際に呼び出してみます。自動生成されたクラスは、my_app_bridge/my_app/service_library/service_1.dart
にあります。インスタンスを作成する際には、RegisterPlatformService
の第2引数で与えた名前を設定します。
final Service1 _service1 = new Service1('my_service');
メソッドの戻り値はFuture
なので、await
を使って値を取得します。
final message = await _service1.hello(name: name);
以下がコードの全体です。
import 'package:flutter/material.dart';
import 'package:my_app_bridge/my_app/service_library/service_1.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final Service1 _service1 = new Service1('my_service');
String _message = '';
@override
void initState() {
super.initState();
_hello('Flutnet');
}
Future _hello(String name) async {
final message = await _service1.hello(name: name);
setState(() {
_message = message;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text(
_message,
style: TextStyle(fontSize: 20),
),
));
}
}
メソッド呼び出しだけではなく、XamarinとFlutterで共通に扱えるデータクラスを定義したり、Xamarin側のイベントをFlutter側で購読することも可能です。
Xamarin
[PlatformData]
public class Data
{
public int Value { get; set; }
}
[PlatformService]
public class EventService
{
[PlatformEvent]
public event EventHandler<ValueChangedEventArgs> ValueChanged;
}
Dart
final data = Data();
final value = data.value;
_eventService.valueChanged.listen((event) {
final value = event.value;
});
ただし、現在のバージョン(Flutnet.Android:1.0.2、Flutnet.iOS:1.0.2)では、PlatformEventを設定すると、RegisterPlatformService
時にArgumentNullException
が発生する様です。おそらくバグなのでその内修正されると思いますが、一応回避策を後述します。
1.0.3で修正されました。
ホットリロード
Flutnetでもホットリロードは可能です。Xamarinプロジェクトで実行した後、Flutterのプロジェクトの方で、Visual Studio Code では、コマンドパレットから、Debug:Attach to Flutter process
を、Android Studioでは、[Run]メニューの[Flutter Attach]を選択します。アタッチされたら、一度再起動を行なった方が良さそうです。これでもでききるのですが、一度デバッグを終了して、再度開始すると、変更が戻ってしまいます。これは先述した通り、Flutterのコードを変更した場合には、再度ビルドを行う必要があるためです。毎回ビルドするのは少々面倒なので、Flutnetには、ロジックの処理を行うXamarinのアプリと画面の確認を行うためのFlutterのアプリ2つ同時に起動して、ローカル通信でデータのやり取りを行うモードが用意されています。
このモード(WebSocketモード)にするためにはAndroid、iOS、Flutterでそれぞれ以下の様に設定します。
_bridge = new FlutnetBridge(flutterEngine, this, FlutnetBridgeMode.WebSocket);
_bridge = new FlutnetBridge(this.Engine, FlutnetBridgeMode.WebSocket);
import 'package:my_app_bridge/flutnet_bridge.dart';
void main() {
FlutnetBridgeConfig.mode = FlutnetBridgeMode.WebSocket;
runApp(MyApp());
}
デバッグを行う際は、Xamarinで実行した後、Flutterの方でも通常のデバッグ実行を行います。通信で行なっているので、少々データの取得まで時間がかかりますが、いちいちビルドする手間が省けるので、画面レイアウトの開発を行う際にはこちらのモードの方が良さそうです。
PlatformEventについて
Flutnet 1.0.1 [BETA]以降は、以下の対応は不要です。
先述しましたが、現状では、PlatformEventを設定できません。どうも内部でリフレクションを使ってメソッドを取得しているのですが、そのメソッドが存在していない様です。おそらくデバッグビルドでは大丈夫だったけど、リリースビルドでリンカーによって削除されたのではないかと思います。やってることが、イベントの登録の様なので、リフレクションで無理やりアクセスして登録する様にしてみました。一応これで動いたので大丈夫だと思います。
var eventService = new EventService();
var name = "event_service";
try
{
FlutnetRuntime.RegisterPlatformService(eventService, name);
}
catch
{
var request = typeof(FlutnetRuntime).GetField("request", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
var creatorListener = request.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance).GetValue(request, new[] { "" });
var interpreter = creatorListener.GetType().GetField("_Interpreter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(creatorListener);
var utilsContainerAuth = interpreter.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance).GetValue(interpreter, new[] { name });
var m_Service = utilsContainerAuth.GetType().GetField("m_Service", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(utilsContainerAuth);
var testPublisherMethod = typeof(FlutnetRuntime).GetMethod("TestPublisher", BindingFlags.NonPublic | BindingFlags.Static);
EventHandler<ValueChangedEventArgs> handler = (object sender, ValueChangedEventArgs e) => testPublisherMethod.Invoke(null, new[]
{
name,
nameof(EventService.ValueChanged),
sender,
e
});
eventService.ValueChanged += handler;
m_Service.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance).SetValue(m_Service, handler, new[] { nameof(EventService.ValueChanged) });
}
var eventService = new EventService();
var name = "event_service";
try
{
FlutnetRuntime.RegisterPlatformService(eventService, name);
}
catch
{
var tag = typeof(FlutnetRuntime).GetField("tag", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
var item = tag.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance).GetValue(tag, new[] { "" });
var m_Getter = item.GetType().GetField("m_Getter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(item);
var @base = m_Getter.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance).GetValue(m_Getter, new[] { name });
var role = @base.GetType().GetField("_Role", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(@base);
var popMessageMethod = typeof(FlutnetRuntime).GetMethod("PopMessage", BindingFlags.NonPublic | BindingFlags.Static);
EventHandler<ValueChangedEventArgs> handler = (object sender, ValueChangedEventArgs e) => popMessageMethod.Invoke(null, new[]
{
name,
nameof(EventService.ValueChanged),
sender,
e
});
eventService.ValueChanged += handler;
role.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance).SetValue(role, handler, new[] { nameof(EventService.ValueChanged) });
}
まとめ
リリースされたばかりで、まだ不安定ではありますが、大きな可能性は感じられます。特にXamarin.Formsで作ったアプリをFlutterで作り直そうとなったときに、ロジック部分はそのまま使えるので、選択肢として大いにありなのではないでしょうか。