preface

We all know that Flutter is an app that can run on both iOS and Android. Obviously, Flutter needs to be able to communicate with Native. For example, your Flutter app displays your phone’s battery count, which can only be obtained through the platform’s system Api. There needs to be a mechanism for Flutter to somehow call the system Api and get the return value. So how does Flutter do it? Platform Channels

Platform Channels

Let’s start with a picture

FlutterView.java

One thing to note here is that messages passed through Platform Channels are asynchronous in order to ensure that the UI responds.

Messages passed on Platform Channels are encoded in several ways, the default being StandardMethodCodec. Others include BinaryCodec (binary encoding, which actually does nothing but returns the input), JSONMessageCodec(JSON encoding), and StringCodec(String encoding). These codecs can only allow the following types:

com.yourmodule.YourObject

How to use Platform Channels

The Platform Channels of Flutter and Native communication have been introduced. So let’s take the use of Platform Channels as a specific example. Here is a Demo of Flutter official to get your phone’s power. The source code can be downloaded from Github.

Platform Channels are the Channels that connect Flutter and Native. To create a Flutter channel, you need to write code on both ends.

MethodChannel

How do I write Native first

MethodChannel – Native client

For simplicity, the Android code for this example is written directly into MainActivity. On the Android platform, the battery is retrieved by calling BatteryManager, so we first add a function to get the battery in the 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;
}
Copy the code

This function needs to be called by the Flutter app. In this case, the MethodChannel needs to be set up. Start by adding the following code to MainActivity’s onCreate function to create a new MethodChannel

Public class MainActivity extends FlutterActivity {// The name of a channel. Since there may be multiple channels in the app, this name needs to be unique within the app. private static final String CHANNEL ="samples.flutter.io/battery"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); // Direct new MethodChannel, Then set a Callback to handle the Flutter side by calling new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new)MethodCallHandler() {@override public void onMethodCall(MethodCall call, Result Result) {// Handle calls from Flutter in this callback}}); }}Copy the code

Note that each MethodChannel needs to be identified with a unique string to distinguish it from each other. This name is recommended for package.module… That’s the pattern. Because all methodChannels are stored in a Map with the channel name Key. So if you set two channels with the same name, only the latter will take effect.

Next, let’s populate the 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(); }}Copy the code

OnMethodCall has two incoming parameters, and MethodCall contains the name of the method to be called and its parameters. Result is the return value to Flutter. The method name is negotiated by both ends. Methodcall. method is distinguished by the if statement, and in our case we will only deal with calls called “getBatteryLevel”. The charge value is returned to the Flutter using the result.success(batteryLevel) call after the local method is retrieved. The Native side of the code is done. Isn’t that easy?

MethodChannel – Flutter

Create a MethodChannel on the Flutter side in 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.
}
Copy the code

The name of the channel must be the same as that of the Native end. Then there’s the code called through the 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;
    });
  }
Copy the code

final int result = await platform.invokeMethod(‘getBatteryLevel’); This line of code calls Native methods through channels. Notice the await keyword here. We said earlier that MethodChannel is asynchronous, so we must use the await keyword here. In the Native code above, we pass result.success(batteryLevel) to obtain the battery; Return to Flutter. Here the power is assigned directly to the result variable after execution of the await expression. The rest is how to display the problem, I won’t go into detail, can look at the code.

Note that here we have only described calling Native methods from Flutter. In fact, Native can also call the methods of Flutter through MethodChannel, which is a two-way channel.

For example, we want to get a string from a getName method on the Native side of the Flutter request. On the Flutter 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; }}Copy the code

On the Native side, you just have the corresponding channel call invokeMethod

channel.invokeMethod("getName", null, new MethodChannel.Result() {@override public void success(Object o)"Hello from Flutter"
            Log.i("debug", o.toString());
          }
          @Override
          public void error(String s, String s1, Object o) {
          }
          @Override
          public void notImplemented() {}});Copy the code

This completes the use of MethodChannel. As you can see, it’s fairly straightforward to call each other with the MethodChannelNative and Flutter methods. This is just a general introduction, the details and some complex uses need to be explored.

MethodChannel provides a channel for method calls. What if Native has a stream of data that needs to be sent to a Flutter? This is where EventChannel comes in.

EventChannel

We also used EventChannel as an example to obtain the battery power of the official demo. The battery status of the mobile phone is constantly changing. We need to inform Flutter of such battery state changes by Native via EventChannel in time. This does not work with the previous MethodChannel method. This means that Flutter needs to poll getBatteryLevel repeatedly to get the current charge, which is obviously not the right way to do this. With EventChannel, the current battery state is “pushed” to Flutter.

The EventChannel – Native client

In mainActivity. onCreate, we add the following code:

new EventChannel(getFlutterView(), "Samples. Flutter. IO/charging") setStreamHandler (new StreamHandler () {/ / receive battery radio BroadcastReceiver. private BroadcastReceiver chargingStateChangeReceiver; Override // This onListen is the callback when the Flutter end starts to listen on this channel. The second EventSink parameter 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 the arguments) {/ / no longer receives the opposite unregisterReceiver (chargingStateChangeReceiver); chargingStateChangeReceiver = null; }});Copy the code

Like MethodChannel, we simply new an EventChannel instance and give it a callback of type StreamHandler. Where onCancel means that the opposite side is no longer receiving, and here we should do something clean up. OnListen means that the channel is set up and Native can send data. Note the EventSink parameter in onListen. Subsequent data sent by Native will pass 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 the status of the Flutter to the events.success(isCharging?"charging" : "discharging"); }}}; }Copy the code

In the onReceive function, after the system sends the battery status broadcast, it is converted to the agreed string in Native, and then by calling events.success(); Send to Flutter. So that’s the Native side of the code. Now look at the Flutter side.

The EventChannel – Flutter

The first step is to create an EventChannel within State

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

Then open this channel at initState:

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

The event is handled in the _onEvent function:

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

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

The “charging”/” Adequacy “string passed from Native is simply the event. Okay, the code for the Flutter side is also posted. Does EventChannel feel easy to use?

finishing

This concludes this article’s explanation of the way that Flutter and Native communicate with each other. The purpose of Flutter is to be cross-platform, and to be truly cross-platform depends on whether Flutter can communicate effectively with Native in a simple way. Whether Platform Channels can achieve this goal has yet to be tested by large-scale application. For Flutter developers, since there are many Native platform apis that need to be exposed to Flutter, there are also many components/business logic implemented with Native that may need to be exposed to Flutter. This requires a lot of channel code, which means that we must master the skill of using Platform Channels to realize the true cross-platform capabilities of Flutter. The application of Platform Channels in this article is a very simple demo. There are two big challenges in a large app, one is how do we organize and maintain a lot of channels. The other is how the channel protocol can be designed to bridge the platform differences between Android and iOS, which requires development that is familiar to both platforms, which seems more difficult.

Of course, if you make the perfect channel, wrap one of the platform’s features (bluetooth, GPS, etc.) into a nice Flutter API, and hope that other Flutter developers around the world can use it as well. Then you can open up your ideas to others by releasing a Plugin for Flutter. Stay tuned for the next article where I will explain how to develop a Flutter plugin.