How Flutter communicates with Native - Android Perspective

preface

We all know that the app developed by Flutter can run on iOS and Android systems at the same time. Obviously, Flutter needs the ability to communicate with Native. For example, your fleet app needs to display the power of your mobile phone, and the power can only be obtained through the platform's system Api. At this time, a mechanism is needed so that Flutter can call the system Api in some way and obtain the return value. So how does Flutter do it? The answer is Platform Channels.

Platform Channels

Let's take a look at this picture first

The above figure comes from the official website of Flutter and shows the architecture diagram of Platform Channels. Careful students will ask. Didn't you say that the communication between fluent and Native is through Platform Channels? How does the MethodChannel connect them in the architecture diagram? In fact, MethodChannel is a kind of Platform Channels. As the name suggests, MethodChannel should be similar to method call. So there are other channels? Yes, there are EventChannel, BasicMessageChannel, etc. If you need to send data from Native platform to fluent, we recommend you use EventChannel. The Flutter framework is also using these channels to communicate with Native. For details, please refer to flutterview Java, you can see more usage of Platform Channels here.

 

It should be noted here that in order to ensure the response of the UI, the messages delivered through Platform Channels are asynchronous.

Messages delivered on Platform Channels are encoded, and there are several encoding methods. The default is to use StandardMethodCodec. Others include BinaryCodec (binary coding, which actually does nothing, and directly returns the input parameters), JSONMessageCodec(JSON format coding), and StringCodec(String format coding). These codecs allow only the following types:

So if you want to define your own com yourmodule. An instance of yourobject type cannot be directly thrown to Platform Channels for transmission.

 

How to use Platform Channels

The Platform Channels of Flutter and Native communication were briefly introduced earlier. Let's talk about the use of Platform Channels with specific examples. Here, we use the Demo of obtaining the power of mobile phone issued by the official Flutter. The relevant source code can be downloaded from Github Download.

Obviously, if we want to establish the connection between the platform and the channel, we should write the code at both ends of the platform.

MethodChannel

Let's see how the Native end is written first

Methodchannel native end

For simplicity, the Android side code of this example is written directly in MainActivity. The power is obtained by calling BatteryManager on Android platform, so we first add a function to obtain power in MainActivity:

 

private int getBatteryLevel() {
  int batteryLevel = -1;
  if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
    BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
    batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
  } else {
    Intent intent = new ContextWrapper(getApplicationContext()).
        registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
        intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
  }

  return batteryLevel;
}

This function needs to be called by the fluent app. At this time, this channel needs to be established through MethodChannel.
First, add the following code to the onCreate function of MainActivity to create a new MethodChannel

 

public class MainActivity extends FlutterActivity {
    //The name of the channel. Since there may be multiple channels in the app, this name needs to be unique in the app.
    private static final String CHANNEL = "samples.flutter.io/battery";

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);
        
        // Directly create a new MethodChannel, and then set a Callback to handle the call on the fluent side
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
                new MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, Result result) {
                        // Handle the call from Flutter in this callback
                    }
                });
    }
}

Note that each MethodChannel needs to have a unique string as its identifier to distinguish it from each other. Package. Is recommended for this name module... Such a pattern is named. Because all methodchannels are saved in the Map with the channel name as Key. So if you set two channels with the same name, only the later one will take effect.

Next, let's fill in onMethodCall.

 

@Override
public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("getBatteryLevel")) {
        int batteryLevel = getBatteryLevel();

        if (batteryLevel != -1) {
            result.success(batteryLevel);
        } else {
            result.error("UNAVAILABLE", "Battery level not available.", null);
        }
    } else {
        result.notImplemented();
    }
}               

onMethodCall has two input parameters. MethodCall contains the method name and parameters to be called. Result is the return value to fluent. The method name is negotiated at both ends. Judge MethodCall by if statement Method to distinguish different methods. In our example, we will only deal with the call named "getBatteryLevel". After calling the local method to obtain the power, use result The success (batterylevel) call returns the power value to the Flutter.
The Native code is completed. Isn't it simple?

Methodchannel fluent end

Next, let's look at how to write the code on the Flutter side:
First, create the MethodChannel at the Fletter end in the State

 

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.io/battery');

  // Get battery level.
}

The name of the channel should be consistent with that of the Native end.
Then there is the code called through MethodChannel

 

String _batteryLevel = 'Unknown battery level.';

  Future<Null> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

final int result = await platform.invokeMethod('getBatteryLevel'); This line of code calls the Native method through the channel. Notice the await keyword here. As we said earlier, MethodChannel is asynchronous, so the await keyword must be used here.
In the above Native code, we get the power through result success(batteryLevel); Return to Flutter. Here, after the await expression is executed, the power is directly assigned to the result variable. What's left is how to show it. I won't go into detail. You can look at the code for details.

It should be noted that here we only introduce the call of Native methods from Flutter. In fact, Native can also call Flutter's methods through MethodChannel, which is a two-way channel.

For example, we want to request a getName method on the Native side and the fluent side to get a string. On the fluent side, you need to set a MethodCallHandler for the MethodChannel

 

_channel.setMethodCallHandler(platformCallHandler);

Future<dynamic> platformCallHandler(MethodCall call) async {
       switch (call.method) {
             case "getName":
             return "Hello from Flutter";
             break;
       }
}

On the Native side, you only need to make the corresponding channel call invokeMethod

 

channel.invokeMethod("getName", null, new MethodChannel.Result() {
          @Override
          public void success(Object o) {
            // "Hello from fluent" will be output here
            Log.i("debug", o.toString());
          }
          @Override
          public void error(String s, String s1, Object o) {
          }
          @Override
          public void notImplemented() {
          }
        });

So far, the usage of MethodChannel has been introduced. It can be found that it is quite direct to call each other through MethodChannelNative and fluent methods. Here is just a general introduction. The specific details and some complex usages need to be explored.

MethodChannel provides a channel for method calls. What if Native has a data stream that needs to be transmitted to fluent? At this time, the EventChannel will be used.

EventChannel

For the use of EventChannel, we also take the official demo of obtaining battery power as an example. The battery state of mobile phones keeps changing. We need to inform Flutter of such battery state changes by Native through EventChannel in time. In this case, the previously mentioned MethodChannel method is not applicable, which means that the Flutter needs to call getBatteryLevel continuously in the way of polling to obtain the current power, which is obviously incorrect. The way of using EventChannel is to "push" the current battery state to the Fletter

EventChannel - Native end

Let's first look at how we are familiar with the Native side to create an EventChannel, or in mainactivity Oncreate, we add the following code:

 

new EventChannel(getFlutterView(), "samples.flutter.io/charging").setStreamHandler(
        new StreamHandler() {
          // BroadcastReceiver that receives battery broadcast.
          private BroadcastReceiver chargingStateChangeReceiver;
          @Override
         // This onListen is the callback when the Fletter side starts listening to this channel. The second parameter EventSink is the carrier used to transmit data.
          public void onListen(Object arguments, EventSink events) {
            chargingStateChangeReceiver = createChargingStateChangeReceiver(events);
            registerReceiver(
                chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
          }

          @Override
          public void onCancel(Object arguments) {
            // Opposite no longer receive
            unregisterReceiver(chargingStateChangeReceiver);
            chargingStateChangeReceiver = null;
          }
        }
    );

Similar to MethodChannel, we also directly create an EventChannel instance and set it with a StreamHandler type callback. onCancel means that the opposite side is no longer receiving. Here we should do some clean up. onListen means that the channel has been built and Native can send data. Note the EventSink parameter in onListen. Subsequent Native data is sent through EventSink. Look at the code:

 

private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) {
    return new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);

        if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
          events.error("UNAVAILABLE", "Charging status unavailable", null);
        } else {
          boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                               status == BatteryManager.BATTERY_STATUS_FULL;
          // Send battery status to Flutter
          events.success(isCharging ? "charging" : "discharging");
        }
      }
    };
  }

In the onReceive function, after the system sends the battery status broadcast, it will be converted into the agreed string here in Native, and then call events success(); Send to Flutter. This is the code of the Native side. Next, let's look at the fluent side.

EventChannel - Flutter end

First, create an EventChannel in the State

 

static const EventChannel eventChannel =
      const EventChannel('samples.flutter.io/charging');

Then open this channel in initState:

 

@override
  void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }

The processing after receiving the event is in_ In onEvent function:

 

void _onEvent(Object event) {
    setState(() {
      _chargingStatus =
          "Battery status: ${event == 'charging' ? '' : 'dis'}charging.";
    });
  }

  void _onError(Object error) {
    setState(() {
      _chargingStatus = 'Battery status: unknown.';
    });
  }

The "charging"/"discharging" string passed from the Native side is the input parameter event directly. Well, the code on the fluent side has also been pasted. Does it feel that the EventChannel is also very simple to use?

Finishing

So far, the explanation of the way of communication between fluent and Native will come to an end. The starting point of Flutter is cross platform, and the real cross platform depends on whether Flutter can effectively communicate with Native in a simple way. Whether Platform Channels can achieve this goal remains to be tested by large-scale applications. For Flutter developers, many Native platform API s need to be exposed to Flutter, and many components / business logic implemented with Native may also need to be exposed to Flutter. This requires writing a lot of channel code, which means that we must master the skills of using Platform Channels to realize the real cross platform ability of Flutter. The application of Platform Channels in this article is only a very simple demo. There are still two major challenges in large apps. One is how to organize and maintain a large number of channels. The other is how to design the channel protocol to smooth out the platform differences between Android and iOS, which requires development. It seems more difficult to be familiar with both platforms.

Of course, if you create a perfect channel, package a function of the platform (such as Bluetooth and GPS) into a beautiful Flutter API, and hope that other Flutter developers in the world can also use it. Then you can open the crystallization of your wisdom to others by publishing the fluent plug-in. In the next article, I will introduce how to develop a Flutter plug-in. Please look forward to it.




Link: https://www.jianshu.com/p/d9eeb15b3fa0
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.

 

 

 

 

Tags: Flutter

Posted by eggradio on Sun, 08 May 2022 14:00:46 +0300