banner
jzman

jzman

Coding、思考、自觉。
github

Detailed Explanation of Platform Channel in Flutter Series

PS: In many situations, 80% of known effects come from 20% of 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, let's introduce the use of Platform Channels in Flutter hybrid development, with the main content as follows:

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

Introduction to Platform Channels#

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 decoder. All decoders support empty messages, and the communication architecture between Native and Flutter is shown in the diagram below:

image

Three different types of Platform Channels 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 implemented. Here is a case where BasicMessageChannel is used to access Flutter resource files in an Android project, with the key process as follows:

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

The key code on the Flutter side is as follows:

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

// Get 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)
    }

    // Set method handler for Flutter to jump to Android
    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 transmission of both Native methods and Dart methods. That is, 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

The following demonstrates how to use MethodChannel to jump from Flutter to the Android native interface MainActivity, with the Android side as follows:

/**
 * @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, with the Flutter side 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

The following demonstrates how to call the Dart method getName in Flutter using MethodChannel, with the Android side code as follows:

/**
 * @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 listeners. Without further ado, let's look at the code, with the Android side code 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.