Introduction
Flutter is a cross-platform toolkit, yet it allows you the full power of native platform APIs in a very simple way. Even if you don’t have any past experience writing native code, be scared no more. In this guide, you will learn everything you need to call native APIs from within your Dart or Flutter project.
Flutter and the hosting platform
There are a couple of concepts to understand about the different ways Flutter communicates with the host native platform.
Platform-aware Dart code
Dart and Flutter are aware of the host platform. This means you can write some platform-aware conditional code such as:
import 'dart:io';
if(Platform.isIOS) {
// a code block that will only be executed on iOS.
} else if(Platform.isAndroid) {
// a code block that will only be executed on Android.
} else {
// on all other platforms.
}
In the previous snippet, note the import dart:io
. The dart:io
library in Dart isn’t available on web, hence there’s no Platform.isWeb
type.
In Flutter, it’s encouraged to avoid importing dart:io
whenever possible. You can write the previous code using Flutter libraries:
import 'package:flutter/foundation.dart';
if(defaultTargetPlatform == TargetPlatform.iOS) {
// a code block that will only be executed on iOS.
} else if(defaultTargetPlatform == TargetPlatform.android) {
// a code block that will only be executed on Android.
} else {
// on all other platforms.
}
TargetPlatform
doesn’t have a web
type as well. To write a web-specific code, there’s a constant available only in Flutter, kIsWeb
.
import 'package:flutter/foundation.dart';
if(kIsWeb) {
// a code block that will only be executed on iOS.
} else if(defaultTargetPlatform == TargetPlatform.iOS) {
// a code block that will only be executed on iOS.
} else if(defaultTargetPlatform == TargetPlatform.android) {
// a code block that will only be executed on Android.
} else {
// on all other platforms.
}
If we look into the implementation of this constant inside the Flutter foundation library:
const bool kIsWeb = identical(0, 0.0);
It’s making use of the fact that JavaScript doesn’t support doubles. Dart on web is compiled to JavaScript, thus it’s backed with the same types of objects. Doubles aren’t identical to integers in Dart on other platforms or on the server side. Read the full explanation in the Flutter API.
Communicate with native APIs
Flutter has a rich ecosystem with hundreds of packages that cover almost all essential use-cases. But, there might come a time where you don’t find a plugin you need on pub.
Let’s imagine a common use-case. A client wants to integrate with a 3rd-party provider. This provider doesn’t have support for Flutter yet, but they have an iOS and Android SDK. In such case, you will need to call these SDKs from Flutter.
Platform channels
The answer to the use-case from the previous section starts with Platform Channels.
From the official documentation:
Messages are passed between the client (UI) and host (platform) using platform channels.
A platform channel is a flexible messaging API between a Flutter app and the platform hosting the app. It’s how you can communicate with native and 3rd-party APIs.
There’re libraries with everything you need to define a channel and communicate back and forth on both Dart, and the hosting platform. For example, to define a channel by name between Dart and Swift (iOS):
- The Dart side:
const _channelName = 'app.id.com/channel_name';
final _channel = const MethodChannel(_channelName);
2. The iOS side:
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "app.id.com/channel_name", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: channel)
}
Note the classes on both sides have a similar API. This makes it easier to have a very similar and familiar code on all the platforms. So, even if you don’t know Kotlin, the following snippet have the same API as the previous from Swift:
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app.id.com/channel_name")
channel.setMethodCallHandler(this)
}
Supported platforms and native languages
Flutter supports 6 platforms. Similarly, platform channels are also available on all supported platforms except web. The following table shows the native languages you can use with each corresponding platform.
Platform | Languages |
---|---|
iOS | Swift or Objective-C |
Android | Kotlin or Java |
macOS | Swift or Objective-C |
Windows | C++ |
Linux | C |
What about web?
Dart is compiled to JavaScript on web, therefore, there is no need for platform channels. The interoperability between Dart and JS makes it possible to call JS functions directly using package:js
.
Supported data types and the standard messaging codec
When you call a method, you either send data (input), expect data (output), or both. This also applies on method channels. You can send some data when calling a method channel from Dart, and receive from the other side. The question here is: if communication is happening between two different languages, what data types are available?
Messages are serialized and deserialized using a standard message codec as simple JSON-like values such as boolean, numbers, Strings, byte buffers, Lists and Maps. This means:
- You can’t send or receive complex typed objects, e.g. user-defined types.
- The data is not type-safe, therefore you need to check for types and nulls on both sides of the channel.
Check the official documentation here for the full list of data types received on each platform and the corresponding type from the Dart side.
Setting up Platform Channels
Platform channels are uniquely identified by name. This name is a String, if you misspelled it in either side of the channel, you won’t be able to reach out to the other side.
There are 2 types of platform channels:
- MethodChannel: a channel used to communicate with the platform using asynchronous method calls. Think of it as a Future.
- EventChannel: a channel used to listen to event streams from the platform. Think of it as a Stream.
MethodChannel
To setup a method channel in Dart:
// Give it a name.
const _channelName = 'app.id.com/channel_name';
// Construct it.
final _channel = const MethodChannel(_channelName);
Once you have a MethodChannel object ready, you can call any method by name:
Future<int> result = _channel.invokeMethod<int>('getBatteryLevel', {
'key': value,
});
EventChannel
To setup an event channel in Dart:
// Give it a name.
const _channelName = 'app.id.com/channel_name';
// Construct it.
final _channel = const EventChannel(_channelName);
Then, start listening to events on the channel:
events.receiveBroadcastStream().listen(
(arguments) {
_handleChangesListener(arguments);
},
);
Each event you will receive will have the arguments
sent from the platform side in the listener callback.
There is an important question you need to think about when setting up an event channel: when is the right time to start the platform stream and when to close it?
Streams consume resources if it’s not disposed when no longer needed. That’s why you need to only start one when you need it, and make sure to clean up after you’re done with it.
The right time depends on the purpose this event stream is serving. You might have some event that you want to start at the moment the app is attached to the platform, which is at the time a user launches it. Another event could be needed only when you manually subscribe to it at some point.
To sum it up, you can:
- Start a stream in the first method called when Flutter is attached to the platform. There’s such a method in all the 5 platforms. For example, on iOS it’s the
application()
method inAppDelegate
class. - Use a
MethodChannel
to call a method that will set a listener on the platform stream, and send event on the channel.
What’s next? 🔥
This’s only the introduction. The next part will dive deeper into actual examples for writing platform channels on iOS and Android.