Platform Channel in Flutter — Benefits and Limitations
One of the biggest challenges for mobile cross-platform frameworks is how to achieve native performance and how to help developers create different kinds of features for different devices and platforms with as little effort as possible. In doing so, we need to keep in mind that UX should remain the same but with unique components that are specific for each particular platform (Android and iOS).
Although cross-platform frameworks (in most of these cases) can resolve platform-specific tasks, there is a certain number of tasks which, with custom platform-specific code, can be achieved only through native. The question is, how can those frameworks establish communication between the specific platform and application? The best example is the Flutter's Platform Channel.
What Is Platform Channel and When Should We Use it?
As the Flutter community grows, more and more community plugins and packages that execute platform-specific functionalities appear. If your project requires a specific feature that is not supported in Flutter or it's easier to implement it on the native side, you need to establish communication between native platforms (Android or iOS) and Flutter in order to execute that custom platform-specific code.
Platform Channel operates on the principle of sending and receiving messages without code generation. The communication is bidirectional and asynchronous. The Flutter application (the portion of the app that is written in Dart) in this communication represents a client that sends messages to the host (Android or iOS) and expects a response back, either as success or failure.
When the message is received on the host's side, we can execute the necessary logic in native code (Java/Kotlin for Android or Objective-C/Swift for iOS) or call any platform-specific APIs and send a response back to the Flutter application through the channel. When channels are created, we need to be mindful of naming conventions. The name of the channel in the Flutter application needs to be the same as the one on the native side.
Setup
One of the basic characteristics of Platform Channel is the fact that it is easy to set up and understand both on the Flutter-side, and the native side. It's also well documented and explained in the official documentation.
In order to explain how to setup/create a channel, I will create a simple example of communication between a Flutter app and Android native (Kotlin).
First, we need to create a channel in the Flutter app with an appropriate name. In this case, we can name it "platform_channel":
static const MethodChannel _channel = const MethodChannel('platform_channel');
Then, we need to create a channel on the Android-side with the same name:
xxxxxxxxxx
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val channel = MethodChannel(registrar.messenger(), "platform_channel")
channel.setMethodCallHandler(PlatformChannelPlugin(registrar.activity()))
}
}
Once we create a channel, we need to create a method in our Flutter app in the PlatformChannel
class, which will communicate with native:
xxxxxxxxxx
static Future<String> dummy_func() async {
String result = await _channel.invokeMethod('dummy_func');
return result;
}
Now, to get a response from this function and from native, we need to add this in the Flutter app where we want to collect the result:
xxxxxxxxxx
static Future<String> getDummyFunc() async => await PlatformChannel.dummy_func();
The final step is to provide an implementation for dummy_func
in native:
xxxxxxxxxx
override fun onMethodCall(call: MethodCall, result: Result) {
when {
call.method == "dummy_func" -> result.success(setupDummyFunc(call))
else -> result.notImplemented()
}
}
private fun setupDummyFunc(call: MethodCall): String {
return "return dummy string from native"
}
With this piece of code, we can say that we established basic communication between the Flutter app and native. Of course, this can be extended to provide any implementation that we need. If we want to pass arguments to native functions, we can create a Map
of values and pass it to the invokeMethod
as a second parameter:
xxxxxxxxxx
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent("dummy1", () => “dummy1”);
args.putIfAbsent("dummy2", () => “dummy2”);
args.putIfAbsent("dummy3", () => “dummy3”);
xxxxxxxxxx
static Future<String> dummy_func() async {
String result = await _channel.invokeMethod(‘dummy_func’, args);
return result;
}
Now, we can access those values in native with their IDs:
xxxxxxxxxx
dummy1 = call.argument<String>("dummy1").toString()
dummy2 = call.argument<String>("dummy2").toString()
dummy3 = call.argument<String>("dummy3").toString()
Here, it is essential to mention that if we are planning to create complex communication between Dart and a platform-specific code, which involves the usage of complicated data structures, I strongly suggest using some mechanism for serializing structured data. Luckily, there is a simple solution for this provided by Google. It's called Protocol Buffers.
Protocol buffers are platform and language-neutral mechanisms for data serialization. Even-more, Google provided tutorials on how to use Protocol Buffers for your desired language.
Benefits of Platform Channel
When we are dealing with any communication, whether we are talking about communication between two or more apps, communication inside a single app, or, as in our case, communication with Dart and native code, a logical question arises: is that communication safe and reliable?
The Platform Channel is secured, and that's one of the important benefits that Platform Channel offers. Within our process, there is a memory buffer for communication between Dart and native code, and there is not any interprocess communication required to establish this communication; thus, there is no way that any other outside process can access this communication. This means that Platform Channel has the same level of security as any other native Android or native iOS application.
One of the benefits of Platform Channel is communication, which is itself asynchronous and bidirectional, meaning that Platform Channel doesn't block execution of other tasks that are independent of native code. Once the native code finishes its work, the result will be passed to the Dart, and the appropriate callback will be triggered and vice versa.
Another benefit is serialization and deserialization of values to and from messages that happens automatically when you send and receive values. This represents the valuable benefit of Platform Channel, alongside an ability to use Protocol Buffers for serializing structured data.
Limitations
Currently, the channel method can be called only from a UI thread (from the main Isolate). Calling MethodChannel
and EventChannel
from a spawned Isolate is not possible at this moment. Performing long-running operations on the main thread can cause "junk" on the Flutter application, and the platform-side will block other message channels. Maybe it's possible to create a workaround for this with ports, but the best advice is to avoid heavy lifting work on the main thread until Flutter resolves problems that happen during the call of platform channel methods from another Isolate.
Overview
Overall, Platform Channel represents a way to connect native code with a Flutter app (Dart). It can be used to implement any Flutter missing functionality using a platform-specific code (plugins) and call any APIs. Moreover, it's well documented and well described in the official documentation and continues to be a handy tool in cross-platform development.
Further Reading
- Getting Started With Kotlin.
- A Look at React Native and React.js.