Using Native Code with Flutter
For developers, startups, and established businesses, cross-platform solutions like React Native or Flutter can be a powerful tool. Where once we had to think about coordinating two code bases, we can now focus our resources on just one, saving time and money.
As good as this sounds any right-thinking developer or executive will naturally ask the question, “What happens when that cross-platform solution doesn’t do what I need it to?” Flutter contains a rich ecosystem of plugins to interact with native platforms, but sometimes that plugin isn’t exactly what you need, isn’t up to date, or doesn’t exist.
In situations like this it is good to know you can get yourself out of any corner you are painted in. In this article we’ll cover how to setup asynchronous and streaming connections from your Flutter application to the native platforms using Kotlin and Swift. As an example we will access the pressure sensor on both platforms and stream barometric pressure readings back to flutter. We will use an asynchronous call to first check for the existence of the sensor on the device. Let’s get started.
Setting up the Flutter Application
After creating a new Flutter application replace the MyHomePage class with the code below.
At the very top of our state object we create two channels, one a MethodChannel and the other an EventChannel. These channels are the critical connections that will facilitate communication to and from the native code. A MethodChannel is asynchronous and can be used to make multiple calls. Typically it is only necessary to establish one MethodChannel in your application.
An EventChannel establishes a streaming connection between your Flutter application and the native code. When setting up multiple streams it is common to establish multiple Event Channels.
Both MethodChannel and EventChannel require a unique string key that we will use to identify them in our platform code later. For this application I have used ‘com.julow.barometer/method’ and ‘com.julow.barometer/pressure’ but the value is insignificant as long as they are unique and you use the same values when calling the streams in your platform code.
static const methodChannel = MethodChannel(‘com.julow.barometer/method’);static const pressureChannel = EventChannel(‘com.julow.barometer/pressure’);
The _checkAvailability function will be used to asynchronously check if the sensor is available on the device. Here we use invokeMethod on the MethodChannel to send a message to our platform code that we would like to execute a function. Like the channel, invokeMethod also requires a unique string key to identify it on the platform side, here I have chosen ‘isSensorAvailable’, but the value is not important. In our platform code we will setup a call handler that listens to our method channel and link any message with the key ‘isSensorAvailable’ to a platform specific function.
Future<void> _checkAvailability() async {try {
var available = await methodChannel.invokeMethod('isSensorAvailable'); setState(() {
_sensorAvailable = available.toString();
});} on PlatformException catch (e) {
print(e);
}
}
The _startReading functions is responsible for listening to the pressureChannel as a BroadcastStream and updating the UI as pressure readings come in. The _stopReading function is used to cancel the subscription. This is all that is required in Flutter to listen to streaming events from native code.
_startReading() {
pressureSubscription=pressureChannel.receiveBroadcastStream().listen((event) {
setState(() {
_pressureReading = event;
});
});
}
The remaining Flutter code establishes a minimal user interface to be able to send the asynchronous request to check for the pressure sensor and then, if the sensor exists, to start and stop reading.
Android Specific Setup
Open the android folder of your Flutter application using Android Studio. This will give you better intellisense when writing Kotlin and also allow you to debug your Kotlin code.
To stream data back to the Flutter Application we will need to establish a StreamHandler. To keep our code organized we will add the StreamHandler in a separate Kotlin file. Inside the java folder (yes, even through we are using Kotlin the folder is called java) find the folder that contains the default MainActivity file and right click to add a new Kotlin file called StreamHandler.
Replace the code in StreamHandler.kt with the code below, substitute the top line with you package name if needed.
Our StreamHandler class takes in three inputs, a SensorManager which is an Android object used to manage sensor data, an integer representing the sensor that we are accessing, and an optional interval reading period for which we are providing a default. These inputs are needed to initialize and read the pressure sensor. We will not cover these in detail since we are focussing on Flutter integration with native code, but you can learn more about native Android sensor APIs at https://developer.android.com/guide/topics/sensors/sensors_environment
Our StreamHandler also implements two interfaces, EventChannel.StreamHandler which is unique to Flutter, and SensorEventListener which is part of the native Android platform. The EventChannel.StreamHandler from Flutter requires implementation of the onListen and onCancel methods. Remember when we setup functions in our Flutter app to listen to and cancel a subscription to our EventChannel? This is where we listen to those events in our native code. We still need to connect our StreamHandler to the event channel, but once done, anytime we listen to or stop listening to our EventChannel stream in Flutter, the onListen and onCancel methods will fire here.
Being able to fire the onListen and onCancel events from our Flutter application allows us to startup and shutdown the native sensor api through the SensorManager, but to be able to send data back over to Flutter, there is one more critical piece, the EventSink. The EventSink is our streaming connection to our Flutter application. We first declare it at the top of our class.
private var eventSink: EventChannel.EventSink? = null
We can turn the sink on and off in our onListen and onCancel events by setting it equal to EventSink from the EventChannel or to null
//On Listen
eventSink = events//On Cancel
eventSink = null
Now that we have an an active sink we can easily send data back to Flutter by calling the success method on the EventSink and by passing it any value. In our StreamHandler the onSensorChanged function is implemented from the SensorEventListener interface and will fire whenever the sensor obtains a reading. We use the first line of code to extract the reading in millibars from the event (as specified in the Android API docs) and then pass this value to our sink where it will be immediately sent to Flutter.
override fun onSensorChanged(event: SensorEvent?) {
val sensorValues = event!!.values[0]
eventSink?.success(sensorValues)
}
Once the Flutter-specific onListen, onCancel and EventSink have been established, the rest of the implementation is entirely native, here we implement the barometer sensor according to the API documentation, but if your needs are more complicated a native developer should have no trouble taking it from this point.
Our StreamHandler is complete, but we still need to wire it up to the main entry point for the Android portion of our application and also implement our MethodChannel. In your package folder inside the java folder, open up the MainActivity.kt file and replace its contents with the code below, substitute the top line with you package name if needed.
In the MainActivity we need to implement our asynchronous MethodChannel and link our StreamHandler to our EventChannel, at the top of the class we declare our variables for this as well as the Android specific SensorManager
private val METHOD_CHANNEL_NAME = "com.julow.barometer/method"
private val PRESSURE_CHANNEL_NAME = "com.julow.barometer/pressure"
private var methodChannel: MethodChannel? = null
private lateinit var sensorManager: SensorManager
private var pressureChannel: EventChannel? = null
private var pressureStreamHandler:StreamHandler? = null
While MainActivity is typically the starting point for all Android applications we can see that in a Flutter app the MainActivity extends a base FlutterActivity, this class comes with two methods we can use to setup and teardown resources, configureFlutterEngine and onDestroy. The setupChannels and teardownChannels functions are called respectively by each and perform the tasks to start and stop the channels.
Within our setupChannels function we can initialize our SensorManager as we would in a standard Android application.
To setup the MethodChannel we first initialize it using the messenger object from our FlutterEngine and the channel name and then attach a CallHandler. The CallHandler is either a switch or series of if/else statements. Here we check to see if the method property of the call contains the “isSensorAvailable” string key we setup on the Flutter side. If so, native code is called to check for the existence of the pressure sensor, otherwise it will return an exception stating that method is not implemented.
methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME)
methodChannel!!.setMethodCallHandler{
call,result ->
if (call.method == "isSensorAvailable") {
result.success(sensorManager!!.getSensorList(Sensor.TYPE_PRESSURE).isNotEmpty())
} else {
result.notImplemented()
}
}
Fire up either the Android application (to be able to debug the Kotlin code) or your Flutter application (to be able to debug the Dart code). You should be able to check for the device sensor and receive pressure readings. If you are using an emulator you will need to simulate the pressure change using the advanced tools.
IOS Specific Setup
With the Android code working, let’s move to IOS. As with Android working in the native IDE will be much easier. Using Xcode, open ios/runner/runner.xcworkspace.
As we did in Android we will need to create a StreamHandler for our EventChannel. Right click on the Runner folder, select New File, and then Swift File, name it PresssureStreamHandler. In our Kotlin code we made our StreamHandler generic with pass-in arguments for the sensor id. This is because the same code is used to handle multiple sensors in Android. By making the Handler generic we could re-use it to read ambient temperature, relative humidity, etc. This is not the case in IOS, each sensor needs specific implementation. The barometric pressure can be obtained in IOS from the CMAlitimeter class which is part of the CoreMotion library. As with the native Android code, we will not be going into depth on the Swift code, but you can find out more about the API here https://developer.apple.com/documentation/coremotion.
Inside PressureStreamHandler.swift paste the code below.
Compared to our Android implementation, the Swift code is relatively light. We are again implementing a FlutterStreamHandler interface which requires an implementation of the onListen and onCancel event. We again use these events to start and stop our sensor through native code. Unlike our Kotlin code, we are not forced to implement separate methods for our sensor and so do not need to create an eventSink variable. Instead we can simply use the EventSink object called events which is passed to us with our OnListen. Where we used the .success method of the sink in Android, here we can simply place our data in the event sink to stream it back to Flutter. IOS uses a different measurement of pressure which is 1/10th of that used by Android. To level that out we multiply the sensor values by 10 when placing in the sink.
events(pressurePascals!.doubleValue * 10.0)
Just as was the case in Android, we must tie our StreamHandler to our EventChannel and implement the MethodChannel at the entry point of our IOS Application. To do this open the AppDelegate.swift file. Add an import for the CoreMotion library at the top, and insert the Custom code in the marked location from below.
Here, as with the SteamHandler, the implementation is a little simpler than Android. Because the code is light we’ll skip creating separate functions and place our code right in the main startup code.
We begin by declaring our channel names, taking care to make sure they match what we implemented in our Flutter application. We also create an instance of our pressureStreamHandler to be passed to our EventChannel.
let METHOD_CHANNEL_NAME = “com.julow.barometer/method”
let PRESSURE_CHANNEL_NAME = “com.julow.barometer/pressure”
let pressureStreamHandler = PressureStreamHandler()
We create a controller which we will need to obtain our messenger and then declare our method channel by passing messenger and channel name just as we did in our Android code.
let controller : FlutterViewController = window?.rootViewController as! FlutterViewControllerlet methodChannel = FlutterMethodChannel(name: METHOD_CHANNEL_NAME, binaryMessenger: controller.binaryMessenger)
Once our MethodChannel is created, we again attach a handler to it for asynchronous calls. Here we use the CMAltimeter class from the CoreMotion library of Swift to check if the sensor is available when our “isSensorAvailable” method is called, otherwise we return an FlutterMethodNotImplemented exception.
methodChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method {
case "isSensorAvailable":
result(CMAltimeter.isRelativeAltitudeAvailable())
default:
result(FlutterMethodNotImplemented)
}})
Lastly, we declare the PressureChannel and attach our StreamHandler to it and we’re done.
let pressureChannel = FlutterEventChannel(name: PRESSURE_CHANNEL_NAME, binaryMessenger: controller.binaryMessenger)pressureChannel.setStreamHandler(pressureStreamHandler)
Run the application from Xcode to check your Swift code. Use an iphone, if possible, to check the application as the Simulator does not have advanced tools for sensors. If everything works in Xcode, try it out one more time from Flutter application. You should be able to obtain the pressure reading from either an Android or IOS device.
While Flutter is not right for every application, the ability to link your application to native code opens the door to greater possibilities, protects you in the event a plugin isn’t available or doesn’t meet your needs and increases your value as a Flutter or Native developer. A Github repo and YouTube video of this project is available below. Thanks for reading.