LoginSignup
40
30

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-18

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.AndroidFlutnet.Interop.iOSパッケージのバージョンがサポートするFlutterのバージョンとなっているので、必ずそのバージョンのFlutterをインストールするようにしてください。(現在の最新(Flutnet 1.0.1 [BETA])では、1.20.2となります。)

プロジェクトの作成

インストールしたFlutnetのプログラムを起動して、[Next]、[Create]とボタンをクリックしていくとプロジェクトが作成されます。
スクリーンショット 2020-08-17 17.00.27.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

次に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では、MainApplicationOnCreate()、iOSでは、ViewControllerViewDidLoad()で行います。

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);

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

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

メソッド呼び出しだけではなく、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でそれぞれ以下の様に設定します。

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

40
30
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
30