banner
jzman

jzman

Coding、思考、自觉。
github

Detailed Explanation of Platform Channel in Flutter Series

PS: In many situations, 80% of the known effects come from 20% of the possible causes.

The previous articles introduced the basics of Flutter development, including the Navigator component, Flex layout, image loading, Widget lifecycle, and hybrid development. The articles are as follows:

Next, we will introduce the use of Platform Channel in Flutter hybrid development, with the main content as follows:

  1. Introduction to Platform Channel
  2. Correspondence of Platform Data Types
  3. BasicMessageChannel
  4. MethodChannel
  5. EventChannel

Introduction to Platform Channel#

Platform Channel is an asynchronous message channel where messages are encoded into binary messages before being sent, and the received binary messages are decoded into Dart values. The types of messages that can be passed are limited to those supported by the corresponding decoders, and all decoders support empty messages. The communication architecture between Native and Flutter is shown in the figure below:

image

Three different types of PlatformChannel are defined in Flutter, mainly as follows:

  • BasicMessageChannel: Used for data transmission;
  • MethodChannel: Used for method calls;
  • EventChannel: Used for event transmission;

All constructors require specifying a channel identifier, decoder, and BinaryMessenger. BinaryMessenger is a communication tool between Flutter and the platform, used to transmit binary data and set corresponding message handlers.

There are two types of decoders: MethodCodec and MessageCodec, where the former corresponds to methods and the latter corresponds to messages. BasicMessageChannel uses MessageCodec, while MethodChannel and EventChannel use MethodCodec.

Correspondence of Platform Data Types#

Platform Channel provides different message decoding mechanisms, such as StandardMessageCodec for basic data type decoding and JSONMessageCodec for JSON decoding. Automatic conversion occurs during communication between platforms, and the correspondence of data types across platforms is as follows:

image

BasicMessageChannel#

BasicMessageChannel is mainly used for data transmission, including binary data. With BasicMessageChannel, the functionalities of MethodChannel and EventChannel can be achieved. Here, we use BasicMessageChannel to implement a case where an Android project uses Flutter resource files, with the key process as follows:

  1. The Flutter side obtains the binary data corresponding to the image resource, using BinaryCodec, resulting in data formatted as ByteData;
  2. Use BasicMessageChannel to send the data corresponding to the image;
  3. On the Android side, use ByteBuffer to receive it, convert it to ByteArray, and then parse it into a Bitmap for display.

The key code on the Flutter side is as follows:

// Create BasicMessageChannel 
_basicMessageChannel = BasicMessageChannel<ByteData>("com.manu.image", BinaryCodec());

// Obtain ByteData for the image in assets
rootBundle.load('images/miao.jpg').then((value) => {
  _sendStringMessage(value)
});

// Send image data
_sendStringMessage(ByteData byteData) async {
  await _basicMessageChannel.send(byteData);
}

The key code on the Android side is as follows:

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    Log.i(tag, "configureFlutterEngine")
    // Set message handler
    BasicMessageChannel<ByteBuffer>(
        flutterEngine.dartExecutor, "com.manu.image", BinaryCodec.INSTANCE
    ).setMessageHandler { message, reply ->
        Log.i(tag, "configureFlutterEngine > message:$message")
        // Data conversion: ByteBuffer->ByteArray
        val byteBuffer = message as ByteBuffer
        imageByteArray = ByteArray(byteBuffer.capacity())
        byteBuffer.get(imageByteArray)
    }

    // For setting Flutter to jump to Android method handler
    MethodChannel(flutterEngine.dartExecutor, channel).setMethodCallHandler { call, result ->
        Log.i(tag, "configureFlutterEngine > method:${call.method}")
        if ("startBasicMessageChannelActivity" == call.method) {
            // Carry image data
            BasicMessageChannelActivity.startBasicMessageChannelActivity(this,imageByteArray)
        }
    }
}

// Display image from Flutter assets
val imageByteArray = intent.getByteArrayExtra("key_image")
val bitmap = BitmapFactory.decodeByteArray(imageByteArray,0,imageByteArray.size)
imageView.setImageBitmap(bitmap)

Additionally, BasicMessageChannel combined with BinaryCodec supports the transmission of large memory data blocks.

MethodChannel#

MethodChannel is mainly used for method transmission, allowing the passing of both Native methods and Dart methods. This means that MethodChannel can be used to call Android native methods from Flutter and Dart methods from Android, with mutual calls made through the invokeMethod method of MethodChannel. Communication must use the same channel identifier, as detailed below:

  1. Flutter calls Android methods

Below is the implementation of jumping from Flutter to the Android native interface MainActivity using MethodChannel on the Android side:

/**
 * @desc FlutterActivity
 * @author jzman
 */
val tag = AgentActivity::class.java.simpleName;

class AgentActivity : FlutterActivity() {
    val tag = AgentActivity::class.java.simpleName;
    private val channel = "com.manu.startMainActivity"
    private var platform: MethodChannel? = null;

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        Log.d(tag,"configureFlutterEngine")
        platform = MethodChannel(flutterEngine.dartExecutor, channel)
        // Set method handler
        platform!!.setMethodCallHandler(StartMethodCallHandler(this@AgentActivity))
    }

    companion object{
        /**
         * Recreate NewEngineIntentBuilder to ensure effectiveness
         */
        fun withNewEngine(): MNewEngineIntentBuilder? {
            return MNewEngineIntentBuilder(AgentActivity::class.java)
        }
    }

    /**
     * Custom NewEngineIntentBuilder
     */
    class MNewEngineIntentBuilder(activityClass: Class<out FlutterActivity?>?) :
        NewEngineIntentBuilder(activityClass!!)

    /**
     * Implement MethodCallHandler
     */
    class StartMethodCallHandler(activity:Activity) : MethodChannel.MethodCallHandler{
        private val context:Activity = activity
        override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
            if ("startMainActivity" == call.method) {
                Log.i(tag,"arguments:"+call.arguments)
                startMainActivity(context)
                // Callback execution result to Flutter
                result.success("success")
            } else {
                result.notImplemented()
            }
        }
    }
}

As shown above, the MethodChannel.Result object can also be used to callback execution results to Flutter. The Flutter side is as follows:

/// State
class _PageState extends State<PageWidget> {
  MethodChannel platform;

  @override
  void initState() {
    super.initState();
    platform = new MethodChannel('com.manu.startMainActivity');
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      margin: EdgeInsets.fromLTRB(8, 8, 8, 0),
      child: RaisedButton(
        onPressed: () {
          _startMainActivity();
        },
        child: Text("Flutter to Android"),
      ),
    );
  }

  /// Jump to native Activity
  void _startMainActivity() {
    platform.invokeMethod('startMainActivity', 'flutter message').then((value) {
      // Receive returned data
      print("value:$value");
    }).catchError((e) {
      print(e.message);
    });
  }
}
  1. Android calls Dart methods

Below is the implementation of calling the Dart method getName in Flutter using MethodChannel on the Android side:

/**
 * @desc MainActivity
 * @author jzman
 */
class MainActivity : FlutterActivity() {
    private val tag = MainActivity::class.java.simpleName;
    private val channel = "com.manu.startMainActivity"
    private var methodChannel: MethodChannel? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btnGetDart.setOnClickListener {
            getDartMethod()
        }
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        Log.i(tag,"configureFlutterEngine")
        methodChannel = MethodChannel(flutterEngine.dartExecutor,channel)
    }

    private fun getDartMethod(){
        methodChannel?.invokeMethod("getName",null, object :MethodChannel.Result{
            override fun success(result: Any?) {
                Log.i(tag,"success: "+result.toString())
                Toast.makeText(this@MainActivity,result.toString(),Toast.LENGTH_LONG).show()
            }

            override fun error(errorCode: String,errorMessage: String?,errorDetails: Any?) {
                Log.i(tag,"error")
            }

            override fun notImplemented() {
                Log.i(tag,"notImplemented")
            }
        })
    }

    companion object{
        fun startMainActivity(context: Context) {
            val intent = Intent(context, MainActivity::class.java)
            context.startActivity(intent)
        }
    }
}

The Flutter side is as follows:

/// State
class _PageState extends State<PageWidget> {
  MethodChannel platform;

  @override
  void initState() {
    super.initState();
    platform = new MethodChannel('com.manu.startMainActivity');

    // Listen for Android calls to Flutter methods
    platform.setMethodCallHandler(platformCallHandler);
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
  /// Flutter Method
  Future<dynamic> platformCallHandler(MethodCall call) async{
    switch(call.method){
      case "getName":
        return "name from flutter";
        break;
    }
  }
}

EventChannel#

EventChannel is mainly used for one-way calls from Flutter to Native, and its usage is similar to broadcasting in Android. The native interface is responsible for sending events, while the Flutter side registers to listen. Without further ado, let's look at the code. The Android side code is as follows:

/// Android
class MFlutterFragment : FlutterFragment() {
    // Here using Fragment, Activity works the same
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        Log.d(tag,"configureFlutterEngine")
        EventChannel(flutterEngine.dartExecutor,"com.manu.event").setStreamHandler(object:
            EventChannel.StreamHandler{
            override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                Log.i(tag,"configureFlutterEngine > onListen")
                // EventSink sends event notification
                events?.success("event message")
            }

            override fun onCancel(arguments: Any?) {
                Log.i(tag,"configureFlutterEngine > onCancel")
            }
        })
    }

    companion object{
        fun withNewEngine(): NewEngineFragmentBuilder? {
            return MNewEngineIntentBuilder(
                MFlutterFragment::class.java
            )
        }
    }

    class MNewEngineIntentBuilder(activityClass: Class<out FlutterFragment?>?) :
        NewEngineFragmentBuilder(activityClass!!)
}

The Flutter side is as follows:

/// State
class EventState extends State<EventChannelPage> {
  EventChannel _eventChannel;
  String _stringMessage;
  StreamSubscription _streamSubscription;

  @override
  void initState() {
    super.initState();
    _eventChannel = EventChannel("com.manu.event");
    // Listen for Event events
    _streamSubscription =
        _eventChannel.receiveBroadcastStream().listen((event) {
      setState(() {
        _stringMessage = event;
      });
    }, onError: (error) {
      print("event error$error");
    });
  }

  @override
  void dispose() {
    super.dispose();
    if (_streamSubscription != null) {
      _streamSubscription.cancel();
      _streamSubscription = null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("EventChannel"),
          centerTitle: true,
        ),
        body: Center(
          child: Text(_stringMessage == null ? "default" : _stringMessage),
        ));
  }
}

This concludes the use of Flutter Platform Channels.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.