I tried Flutnet which can use Flutter in C # (Xamarin)

7 minute read

Flutter is great, but if I wish I could write it in C #, I’ve released a dream-like tool that makes it possible, so I tried it.

What is Flutnet

Flutnet is a framework that makes it easy to interoperate with Xamarin and Flutter. With Flutnet, you can build a beautiful UI with Flutter, but the logic part is not Dart, but the familiar C #. There is a charge, but there is a limited trial version that you can only use a unique application ID (bundle ID), so you can try it for free.

How to use

Flutnet has GUI tools that make it easy to build projects.

Installation

You can download the tool from here (https://www.flutnet.com/Download/Releases). To use it, you need to pass it through the path of the Flutter or Android SDK.

Setting example (macOS)

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

If you haven’t built an environment such as Visual Studio or Flutter yet, click here for Windows (https://www.flutnet.com/Documentation/Getting-Started/Install-on-Windows), and for macOS. Please refer to here.

important point

Flutnet has a fixed version of Flutter that it supports. The version of the Flutnet.Interop.Android and Flutnet.Interop.iOS packages added to the created project is the supported version of Flutter, so be sure to install that version of Flutter. (Currently the latest (Flutnet 1.0.1 [BETA]) is 1.20.2.)

Creating a project

Start the installed Flutnet program and click the [Next] and [Create] buttons to create the project.
スクリーンショット 2020-08-17 17.00.27.png

It is created with the following folder structure.

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

Below the Flutter folder is the Flutter project, and the others are Xamarin projects. Open MyApp.sln in Visual Studio and my_app in Visual Studio Code or Android Studio with the Flutter plugin. my_app_bridge contains Dart code that is automatically generated from C # code.

Build

The build must be done in both the Xmarin project and the Flutter project. For the time being, build MyApp.Android or MyApp.iOS that opened Visual Studio and try running it. If you see the default Flutter screen below, you are successful. If you don’t see it, your Flutter version may be different.
スクリーンショット 2020-08-17 17.34.15.png

Next, build the Flutter project. Every time you change the code in Flutter, you need to build it. In the terminal, run the following command. (In the case of Visual Studio Code, get the flutter package in advance with Flutter: Get Packages from the command palette)

Android

$ flutter build aar --no-profile

iOS

$ flutter build ios-framework --no-profile

In addition, you will need to rebuild the MyApp.ServiceLibrary project on the Xamarin side for the changes to take effect in Xamarin. In addition, on Android, it seems that it will not be reflected unless you delete the previously installed application.

Use

A class called Service1 is created in the MyApp.ServiceLibrary project. Let’s call this from Flutter.

Service1.cs


[PlatformService]
public class Service1
{
    [PlatformOperation]
    public string Hello(string name)
    {
        return $"Hi, {name}! How are you?";
    }
}

The method with the PlatformOperation attribute in the class with the PlatformService attribute is the target to be called from Flutter. When you build the MyApp.ServiceLibrary project, a Dart class to call this process is automatically created. In addition, you need to register an instance on each platform to call it. On Android, use ʻOnCreate () of MainApplication, and on iOS, use ViewDidLoad () of ViewController`.

FlutnetRuntime.RegisterPlatformService(new Service1(), "my_service");

Since registration is for each plot form, it is also possible to add only the interface to the MyApp.ServiceLibrary project and write and register the actual processing for each plot form.
Now let’s move on to the Flutter project and actually call it. The auto-generated classes are located at my_app_bridge / my_app / service_library / service_1.dart. When creating an instance, set the name given in the second argument of RegisterPlatformService.

final Service1 _service1 = new Service1('my_service');

The return value of the method is Future, so use ʻawait` to get the value.

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

Below is the whole code.

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

In addition to method calls, it is also possible to define data classes that can be handled in common by Xamarin and Flutter, and to subscribe to events on the Xamarin side on the Flutter side.

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

~~ However, in the current version (Flutnet.Android: 1.0.2, Flutnet.iOS: 1.0.2), when PlatformEvent is set, ʻArgumentNullException seems to occur at RegisterPlatformService`. It’s probably a bug, so I think it will be fixed, but I’ll explain the workaround later. ~~
Fixed in 1.0.3.

Hot reload

Hot reload is also possible with Flutnet. After running in a Xamarin project, in the Flutter project, in Visual Studio Code, select Debug: Attach to Flutter process from the command palette, and in Android Studio, select Flutter Attach from the Run menu. .. Once attached, it seems better to reboot once. That’s fine, but once I quit debugging and start again, the changes come back. This is because, as mentioned earlier, if you change the Flutter code, you will need to build again. Since it is a little troublesome to build each time, Flutnet has a mode to start two Xamarin apps that process logic and Flutter apps that check the screen at the same time and exchange data by local communication. It has been.
To switch to this mode (WebSocket mode), set as follows on Android, iOS, and 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());
}

When debugging, after executing with Xamarin, Flutter also performs normal debugging execution. Since it is done by communication, it takes some time to acquire the data, but it saves the trouble of building each time, so this mode seems to be better when developing the screen layout.

About Platform Event

** After Flutnet 1.0.1 [BETA], the following measures are not required. ** **

As mentioned earlier, PlatformEvent cannot be set at this time. I’m getting a method using reflection internally, but it doesn’t seem to exist. It was probably okay in the debug build, but I think it was removed by the linker in the release build. What I’m doing is like registering an event, so I tried to force access and register by reflection. I think it’s okay because it worked with this.

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

Summary

It’s just been released and it’s still volatile, but it has great potential. Especially when you try to recreate an application made with Xamarin.Forms with Flutter, the logic part can be used as it is, so it may be a great choice.