Native integrations with Flutter
Native integrations with Flutter and what they are for
Creating native integrations with Flutter allows the full potential of devices to be exploited, introducing functionality that would otherwise be absent. This can be useful when we intend to develop advanced apps that include background monitoring features, integrations with native libraries, and more.
In this article I will explain how Flutter collaborates with native, what platform channels are, and how we can take advantage of them.
Native functionality in Flutter plugins: what are they?
First, Flutter offers many native features through its plugins. The latter allow developers to access native libraries and functionality without resorting to custom integrations.
Here is a short list of the features that are provided by flutter plugins:
Camera: allows developers to access the device's camera and capture images and video; this is particularly useful for photography apps or scanning QR codes.
Location: allows developers to access the device's location data, enabling the creation of, for example, navigation or location apps.
Sensors: allows developers to access the device's sensors, such as the gyroscope and accelerometer, through which various types of apps, such as fitness apps and games, can be created.
Push notifications: the ability to send push notifications allows developers to build apps that send notifications to users about new content or events.
Bluetooth connection: thanks to this plugin it is possible to build apps that deal with monitoring or controlling external devices via Bluetooth.
These features can be implemented in Flutter simply by installing the plugins already available. However, the situation becomes complicated when the available plugins are not sufficient to achieve the desired goal.
When plugins are not enough: platform channels
What are some difficulties that plugins cannot cope with? Some examples are:
Integration with external SDKs that do not support Flutter,
The monitoring of Bluetooth devices in the background,
Advanced management of push notifications, such as running code when a notification arrives,
Extensions of app operation through system features, such as widgets.
Flutter solves this problem by providing platform channels.
To understand how it works, it is important to note that Flutter is always hosted within a native application specific to the platform in which it is used. Concretely, this means that on iOS Flutter is hosted within an app written in Swift, which instantiates a native component, namely a ViewController, which contains and displays the Flutter app to the user.
Platform channels are an asynchronous communication mechanism useful for sending and receiving messages from the Flutter app to the container app and vice versa. Let us see some of these and their characteristics below.
The message channels are platform channels designed to send information of various types from Flutter to native and vice versa.
At the lowest level, Flutter communicates with the containing app through binary message channels. Usually this type of communication is not used, since the transfer of information through this channel must be done in a binary manner. This means that we will have to write the values that we intend to communicate within a memory buffer and then, upon receiving the message, go and read them by interpreting their contents through the specific APIs that each platform provides.
Here is an example of using the binary channel:
This method sends a message that contains a Float64 and an Int32 and expects a response of type string in UTF-8 format
This is followed by the Swift counterpart, which listens to the message, interprets its content, and sends a response:
Encoding and decoding of data
As can be seen from the example, a lot of code needs to be written to communicate in this way. In addition, the risk of introducing bugs is very high.
For these reasons, Flutter also provides some codecs to facilitate the interpretation of the data we pass. Codecs are software that allows digital data to be encoded and decoded. Some examples are the
.jpeg formats or, as far as the channel is concerned, by setting a
StringCodec() we will then know that the content of the message will be a string.
There are four codecs provided by Flutter:
BinaryCodec: represents the lowest level way of sending data.
StringCodec: allows text messages to be sent in UTF-8 format.
JSONMessageCodec: is for sending objects, including nested objects, in JSON format.
StandardMessageCodec: useful for sending homogeneous lists and other types of objects.
However, it is also possible to create custom codecs as needed.
The method channels are platform channels designed to invoke "named pieces of code." They are useful when we intend to make a native method available to Flutter or vice versa. However, they are not strictly tied to a method; we are the ones who have to read the name within the message and invoke the corresponding method, so it could also be employed to execute a piece of code.
The method channel works very similarly to the message channel, in fact it is equivalent to using a binaryMessageChannel by specifying a special codec, called a method codec, which is tailored based on the method parameters. In addition to the parameters, other information is also specified in the method codec regarding how the response will be handled in case of success or failure and whether an error will occur.
Let us now look at an example in which Flutter calls a native method through a method channel:
Example of a request to the method channel with Dart
Swift counterpart, which receives the request, invokes the corresponding method and provides the response
To further explore the various types of channels and codecs I recommend this article on Medium that discusses platform channels in more detail.
What is Pigeon and how can it help us
Pigeon is a tool that automatically generates the platform channels we need. To tell Pigeon which native methods we want to be accessible on the Flutter side, we need to create an abstract class that contains the method signatures and it will take care of generating the rest.
Thanks to Pigeon, it is possible to connect Dart and native code quickly and easily, without having to manually create platform channels and thus also reducing the probability of introducing bugs.
First of all, it is good to specify that the intention of this example is only to give you an idea of how Pigeon works and what it can do. If you intend to use it in your app, please refer to the official documentation.
Suppose we want to implement in our flutter app an external SDK that is not compatible with Flutter. This SDK allows our app to communicate via Bluetooth with a port in a secure manner. The result we want to achieve is to be able to control the port and display its status through the UI we created in Flutter.
In this case Pigeon does the trick! Let's add it to our project with the command:
dart pub add pigeon.
Once installed, we can proceed to create the abstract class that contains the methods we need, for example:
A script must be created to start automatic generation. The following example generates code for Android and iOS, but you can also do this for other platforms by following the documentation.
If you intend to use Pigeon to connect Flutter with Android and iOS you can copy this script but remember to replace the paths and file names which will be different in your case.
Once the code has been generated, the implementation of the native-side methods can be written. To do this, a class must be created that implements the interface generated by the script.
For example, in Swift we need to create a new class that implements the
DomoticsApi interface we defined earlier.
Once we create this class we have to implement the methods we defined earlier. If we use Xcode, it will directly propose us to implement them with a message like the following:
The result will be something like this:
The last step is to connect the channel to Flutter. To do this, simply implement this function and call it within the
didFinishLaunching method of the AppDelegate.
Once the native implementation is finished, we can access the native methods directly from Flutter.
Example of native openDoor method call and error handling
Flutter has no more limits
Through the use of Pigeon, it is possible to quickly integrate native code into Flutter, which also allows us to take full advantage of the potential of native platforms. In this way, the potential of Flutter is increased to the point where we are able to build virtually any type of application, providing an excellent user experience while maintaining a unified code base.