In this post, we'll take a look at how to run native code in Flutter and provide a quick template to get you started.
Running native code with Flutter is done through the use of a defined platform channel, specifically the MethodChannel. This channel allows the Flutter app to send and receive messages from the host platform (iOS or Android).
The messages are encoded into binary before being sent and binary results received are decoded into Dart values. The data sent through the channel is converted as shown in the following table:
It's important to note that messages and responses are passed asynchronously, but the channel's method needs to be invoked on the main thread. This means that the main thread can experience jank if the method takes too long to compute. However, this can be mitigated by sending messages in different threads on the backend.
Another important characteristic to keep in mind is that Method Channels cannot be used in isolates because the main isolate is the only one running on the main thread.
Now that you have a basic understanding of how to run native code in Flutter, let's take a look at a quick template to get you started.
How to create a methodchannel
Define the methodchannel in the state of the app. The name needs to match.
You can use this as a template for whatever you want to do.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('com.dev.app/channel_name');
}
Platform specific code, creating the 'backend', in the MainActivity.kt or .java create the method channel.
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.dev.app/channel_name"
private fun doSomethingFunction(): Int {
return 10
}
return batteryLevel
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
// Note: this method is invoked on the main thread.
if (call.method == "doSomething") {
val funValue = doSomethingFunction()
if (funValue != -1) {
result.success(funValue)
} else {
result.error("UNAVAILABLE", "Error message.", null)
}
} else {
result.notImplemented()
}
}
}
}
Call method channel in the flutter app, function in the state:
String _valueReceivied= 'default value';
Future<void> _getBatteryLevel() async {
String tempValue;
try {
final int result = await platform.invokeMethod('doSomething');
batteryLevel = 'New data $result';
} on PlatformException catch (e) {
batteryLevel = "Failed to get new data: '${e.message}'.";
}
setState(() {
_valueReceivied = tempValue;
});
}
Then execute the function as you see fit.
You can also use invokeListMethod
and invokeMapMethod
that are an implementation of invokeMethod
however they expect to receive a List or Map. More info.
This way of communicating and executing code with the backend of the platform is not typesafe as it requires the definition of the same datatype in the backend as in the flutter app.
Another way of doing this is using code generation with Pigeon. More info in the pub.dev page. I will try to make a post about this if i find it interesting.
You can make Packages to separate the platform-specific code for the UI and also publish the package to the Flutter ecosystem. Developing, Publishing.
Threading
The main thread and where the method channels are executed is also the UI thread so if some computation takes too long to finish the UI might suffer from this.
So one thing to keep in mind is...
When your app performs intensive work in response to user interaction, this single thread model can yield poor performance unless you implement your application properly. Specifically, if everything is happening in the UI thread, performing long operations such as network access or database queries will block the whole UI. When the thread is blocked, no events can be dispatched, including drawing events. From the user's perspective, the application appears to hang. Even worse, if the UI thread is blocked for more than a few seconds (about 5 seconds currently) the user is presented with the infamous "application not responding" (ANR) dialog. The user might then decide to quit your application and uninstall it if they are unhappy.
You can jump from a background thread to the UI thread by doing this
on Kotlin:
Handler(Looper.getMainLooper()).post {
// Call the desired channel message here.
}
on Java:
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
// Call the desired channel message here.
}
});
Not sure what the docs meant by this, but this might be a way of running the native code in a different thread, although I've tested something similar and I've experienced erros, so more testing needs to be done here, if you have any info let me know.
This is awesome to execute native code or libraries that don't have support in dart, in my example this is awesome for ML libraries and get models to run on the device.
One thing that I've experience is that when running ML models the UI threads struggles so I will make test on running these models on different threads, or making plugins as they run in different threads.
I did a quick introduction on how to use Pytorch mobile on flutter, I'm planning on doing a more comprehensive one. I going to talk about the use of threads for making things faster and without lag if i find that they work properly.
Sources:
Top comments (0)