C#(Xamarin)でFlutterが使えるFlutnetを試してみた

Flutterはとても素晴らしいのですが、C#で書けたらもっといいのになと思っていたら、それが実現できる夢のようなツールがリリースされたので試してみました。

# Flutnetとは
[Flutnet](https://www.flutnet.com/)は、XamarinとFlutterを簡単に相互利用できるようにしたフレームワークです。Flutnetを使えば、Flutterで美麗なUIを構築をしつつも、ロジック部分はDartではなく、使い慣れたC#でということが可能になります。有料なのですが、一意のアプリケーションID（バンドルID）しか使えないなどの制限付きのトライアルバージョンがあるので、試す分には無料でできます。

# 使い方
Flutnetには、GUIツールが用意されていて、簡単にプロジェクトの構築ができます。
## インストール
[ここ](https://www.flutnet.com/Download/Releases)からツールをダウンロードできます。使うには、FlutterやAndroidのSDKのパスを通しておく必要があります。

設定例（macOS）

```sh
export　PATH=$PATH:$HOME/development/flutter/bin:$HOME/Library/Android/sdk/platform-tools
```

Visual StudioやFlutterなどの環境構築がまだの方は、Windowsの場合は、[こちら](https://www.flutnet.com/Documentation/Getting-Started/Install-on-Windows)、macOSの場合は、[こちら](https://www.flutnet.com/Documentation/Getting-Started/Install-on-macOS)を参考に行ってください。

### 注意点
FlutnetはサポートしているFlutterのバージョンが決まっています。作成したプロジェクトに追加される`Flutnet.Interop.Android`、`Flutnet.Interop.iOS`パッケージのバージョンがサポートするFlutterのバージョンとなっているので、必ずそのバージョンのFlutterをインストールするようにしてください。（現在の最新（Flutnet 1.0.1 [BETA]）では、`1.２０.2`となります。）

## プロジェクトの作成
インストールしたFlutnetのプログラムを起動して、[Next]、[Create]とボタンをクリックしていくとプロジェクトが作成されます。
![スクリーンショット 2020-08-17 17.00.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236182/620d1b2e-ed4e-eb79-4689-a07ad80a9a74.png)

以下のようなフォルダ構成で作成されます。

```
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のバージョンが異なっている可能性があります。
![スクリーンショット 2020-08-17 17.34.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236182/47fcad6e-ff07-479e-6d07-267757422e44.png)

次に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から呼び出すようにします。

```Service1.cs
[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()`で行います。

```C#
FlutnetRuntime.RegisterPlatformService(new Service1(), "my_service");
```

登録はプロットフォーム毎なので、`MyApp.ServiceLibrary`プロジェクトにはインターフェースのみ追加して、実際の処理は、プロットフォーム毎に書いて登録するということも可能です。
では、Flutterプロジェクトに移り、実際に呼び出してみます。自動生成されたクラスは、`my_app_bridge/my_app/service_library/service_1.dart`にあります。インスタンスを作成する際には、`RegisterPlatformService`の第２引数で与えた名前を設定します。

```dart
final Service1 _service1 = new Service1('my_service');
```
メソッドの戻り値は`Future`なので、`await`を使って値を取得します。

```dart
final message = await _service1.hello(name: name);
```

以下がコードの全体です。

```main.dart
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),
          ),
        ));
  }
}
```

![スクリーンショット 2020-08-18 19.42.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236182/334a29bc-ed2a-f1f8-2e28-571c07dac119.png)

メソッド呼び出しだけではなく、XamarinとFlutterで共通に扱えるデータクラスを定義したり、Xamarin側のイベントをFlutter側で購読することも可能です。

Xamarin

```C#
[PlatformData]
public class Data
{
    public int Value { get; set; }
}
```

```C#
[PlatformService]
public class EventService
{
    [PlatformEvent]
    public event EventHandler<ValueChangedEventArgs> ValueChanged;
}
```

Dart

```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のアプリ２つ同時に起動して、ローカル通信でデータのやり取りを行うモードが用意されています。
このモード（WebSocketモード）にするためにはAndroid、iOS、Flutterでそれぞれ以下の様に設定します。

```MainApplication.cs
_bridge = new FlutnetBridge(flutterEngine, this, FlutnetBridgeMode.WebSocket);
```

```ViewController.cs
 _bridge = new FlutnetBridge(this.Engine, FlutnetBridgeMode.WebSocket);
```

```main.dart
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を設定できません。どうも内部でリフレクションを使ってメソッドを取得しているのですが、そのメソッドが存在していない様です。おそらくデバッグビルドでは大丈夫だったけど、リリースビルドでリンカーによって削除されたのではないかと思います。やってることが、イベントの登録の様なので、リフレクションで無理やりアクセスして登録する様にしてみました。一応これで動いたので大丈夫だと思います。

```MainApplication.cs
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) });
}
```

```ViewController.cs
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で作り直そうとなったときに、ロジック部分はそのまま使えるので、選択肢として大いにありなのではないでしょうか。