This feature has not been described in the documentation, but it is a very useful feature. It is also possible to embed a native View into a Flutter layout. This feature allows us to embed dual platform native Autonavi or Baidu Map, or even the SDK for camera preview video calls, into the Flutter layout. This article uses TextView as an example to show how to embed native components in the Flutter project.

Create Flutter project

The standard way to write a native component extension is to create a plugin project and then introduce the Flutter project into the plugin project. For convenience, this article will write and register components directly in the Flutter project. The development of the plugin project will be introduced later. Create a normal Flutter project using AndroidStudio. Modify the main.dar file to remove unnecessary code for demonstration purposes.

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')); }}class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}


class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center( ), ); }}Copy the code

Write and register native components in the Android project

The process for adding native components is basically as follows: 1. Implement native components PlatformView provides native View 2. Create PlatformViewFactory to generate PlatformView 3. Create a FlutterPlugin to register native components

Creating native components

There are several folders generated in the FLutter project. Lib is to put the FLutter project code. The Android and ios folders are the corresponding dual platform native projects, respectively. Project the default generated GeneratedPluginRegistrant and MainActivity two files, GeneratedPluginRegistrant don’t move, under the package and MainActivity new custom View, The native View of Flutter cannot inherit directly from the View. It needs to implement the provided PlatformView interface:

public class MyView implements PlatformView {

    private final TextView myNativeView;

    MyView(Context context, BinaryMessenger messenger, int id, Map<String.Object> params) {
        TextView myNativeView = new TextView(context);
        myNativeView.setText("I'm a native TextView from Android.");
        this.myNativeView = myNativeView;
    }

    @Override
    public View getView() {
        return myNativeView;
    }

    @Override
    public void dispose() {

    }
}
Copy the code

This is a wrapper class that returns the native View object to Flutter in the implementation of the getView method. For demonstration purposes, this class returns a TextView.

Create PlatformViewFactory

Next create PlatformViewFactory, create a class that inherits from PlatformViewFactory:

public class MyViewFactory extends PlatformViewFactory {
    private final BinaryMessenger messenger;

    public MyViewFactory(BinaryMessenger messenger) {
        super(StandardMessageCodec.INSTANCE);
        this.messenger = messenger;
    }

    @SuppressWarnings("unchecked")
    @Override
    public PlatformView create(Context context, int id, Object args) {
        Map<String.Object> params = (Map<String.Object>) args;
        return new MyView(context, messenger, id, params);
    }
Copy the code

The create method can obtain three parameters. Args is a custom parameter passed by Flutter and is not used here.

To register the plugin

Create a plug-in class MyViewFlutterPlugin and write the registration logic in the static method of the class to be called:

public class MyViewFlutterPlugin {
    public static void registerWith(PluginRegistry registry) {
        final String key = MyViewFlutterPlugin.class.getCanonicalName();

        if (registry.hasPlugin(key)) return;

        PluginRegistry.Registrar registrar = registry.registrarFor(key);
        registrar.platformViewRegistry().registerViewFactory("plugins.nightfarmer.top/myview".newMyViewFactory(registrar.messenger())); }}Copy the code

Code above USES the plugins. Nightfarmer. Top/myview a string, so this is the registration name, component in Flutter call when needed, you can use any format string. Add a registration call to MainActivity’s onCreate method

 MyViewFlutterPlugin.registerWith(this);
Copy the code

Since this is written directly in the Flutter project, it is also possible to write the registration logic directly into the Activity. In order to be consistent with the registration process of the plugin project, it is recommended to write it out.

Call the native View in the Flutter project

Native View call is very simple, in the use of Android platform View just need to create AndroidView component and tell it the component registration name:

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: AndroidView(viewType: 'plugins.nightfarmer.top/myview'),),); }}Copy the code

Because just realized the Android platform, so here and call the AndroidView directly, if you are a pair of platform implementation, will be introduced to package: flutter/foundation. The dart package, And determine whether defaultTargetPlatform is targetPlatform. android or targetPlatform. iOS to introduce different platform implementations.

Add parameters to the native view

In some cases, we need to provide some initialization parameters to the native component, such as the URL of the WebView, such as the center coordinates of the map, and such as the text content of the above example. We can do this by passing in a map:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print(defaultTargetPlatform);
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: AndroidView(
          viewType: 'plugins.nightfarmer.top/myview'.creationParams: {
            "myContent": "Text content passed in as a parameter",},creationParamsCodec: constStandardMessageCodec(), ), ), ); }}Copy the code

CreationParams passes in a map parameter, which is received by the native component, and creationParamsCodec passes in a coded object, which is the way it’s written. We then receive the parameters in the native component and initialize the text of the TextView:

public class MyView implements PlatformView {

    private final TextView myNativeView;

    MyView(Context context, BinaryMessenger messenger, int id, Map<String.Object> params) {
        TextView myNativeView = new TextView(context);
        myNativeView.setText("I'm a native TextView from Android.");
        this.myNativeView = myNativeView;
        if (params.containsKey("myContent")) {
            String myContent = (String) params.get("myContent"); myNativeView.setText(myContent); }}... }Copy the code

It is important to note that the parameters initialized by the native component are not repeatedly assigned with setState, meaning that these are init parameters. For how to change the state of a native component that has been instantiated, see MethodCall

Communicate with native components through MethodChannel

Let’s start with the original component implementing the MethodCallHandler interface:

public class MyView implements PlatformView.MethodChannel.MethodCallHandler {

    private final TextView myNativeView;

    MyView(Context context, BinaryMessenger messenger, int id, Map<String.Object> params) {
		...
        MethodChannel methodChannel = new MethodChannel(messenger, "plugins.nightfarmer.top/myview_" + id);
        methodChannel.setMethodCallHandler(this);
    }
    
    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
         // Calls from Flutter can be received in the callback method of the interface}... }Copy the code

Then do the following in the DART code:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print(defaultTargetPlatform);
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: AndroidView(
          viewType: 'plugins.nightfarmer.top/myview'.creationParams: {
            "myContent": "Text content passed in as a parameter",},creationParamsCodec: const StandardMessageCodec(),
          onPlatformViewCreated: onMyViewCreated,
        ),
      ),
    );
  }

  MethodChannel _channel;

  void onMyViewCreated(int id) {
    _channel = new MethodChannel('plugins.nightfarmer.top/myview_$id');
    setMyViewText();
  }

  Future<void> setMyViewText(String text) async{ assert(text ! =null);
    return _channel.invokeMethod('setText', text); }}Copy the code

Using the onPlatformViewCreated callback, we can listen for the original component to be successfully created and get the current component ID in the callback method argument, which is randomly assigned by the system. We can then create a MethodChannel that communicates with the component using the assigned ID prefixed with our component name. Once you’ve got the channel object, you can send a message to the native component using the invokeMethod method. Here, you send a ‘setText’ message with text content, and then you handle the logic of receiving the message in the native component.

    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
        if ("setText".equals(methodCall.method)) {
            String text = (String) methodCall.arguments;
            myNativeView.setText(text);
            result.success(null); }}Copy the code

The onMethodCall is handled the same way as a normal plug-in extension and won’t be covered here.

How the performance

Instantiate multiple native components using a ListView and see what happens:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print(defaultTargetPlatform);
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          return Container(
            child: AndroidView(
              viewType: 'plugins.nightfarmer.top/myview'.creationParams: {
                "myContent": "Text content passed in as an argument $index",},creationParamsCodec: const StandardMessageCodec(),
            ),
            height: 100,); },itemCount: 100,),); }}Copy the code

Instantiation of multiple native components in an interface has a significant impact on performance. It is not recommended to introduce a large number of native components in the actual development, because except for special cases such as maps/WebViews, Basically any UI effect that can be implemented natively can be implemented by the Flutter UI engine.

The thermal loading of Flutter is not effective when developing native components because the native project needs to be compiled each time for it to work. In addition, my Mac environment doesn’t work properly with Genymotion, so I need to use the enable-software-rendering parameter instead of the real machine.

Finish this.


More dry goods move to my personal blog www.nightfarmer.top/