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:
- Using the Navigator Component in Flutter Series
- Detailed Explanation of Flex Layout in Flutter Series
- Detailed Explanation of Image Loading in Flutter Series
- Widget Lifecycle in Flutter Series
- Hybrid Development in Flutter Series - Android Edition
Next, we will introduce the use of Platform Channel in Flutter hybrid development, with the main content as follows:
- Introduction to Platform Channel
- Correspondence of Platform Data Types
- BasicMessageChannel
- MethodChannel
- 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:
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:
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:
- The Flutter side obtains the binary data corresponding to the image resource, using BinaryCodec, resulting in data formatted as ByteData;
- Use BasicMessageChannel to send the data corresponding to the image;
- 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:
- 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);
});
}
}
- 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.