Exploring Flutter’s communication with native code

How seasmless is it for Flutter to talk to Native iOS and Android code?

How seasmless is it for Flutter to talk to Native iOS and Android code?

Intro

This blog investigates how Flutter communicates with native code and explores the complexities, challenges, and risks involved. As an example, we want to get a list of tasks from a native SDK and display them on screen.

Most cross-platform frameworks also follow a similar communication path, so the lessons learnt here mostly apply for them as well.

Flutter communication flow

Flutter implementation

We want to get the tasks and show them in a widget:class MyApp extends StatelessWidget {
@override Widget build(BuildContext context) {
let tasks = TasksTarget.getTasks(5931)
setTasks(tasks)
}
}

This requires us to make an in-between target that handles invoking the native functions and decoding them from JSON.class TasksTarget {static Future<List<Task>> getTasks(int userId) async {
const platform = const MethodChannel('samples.flutter.dev/tasks');try {
final List<dynamic>? tasks = await _channel.invokeMethod<List<dynamic>>('getTasks', userId);
return tasks?.map(Task.fromJson).toList() ?? <Task>[];
} on PlatformException catch (e) {
// TODO handle the error here
}
return <Task>[]
}
}

Native implementation

Before we begin on the native side, we need to register a channel to communicate on:

// In App delegate's didFinishLaunchingWithOptions:
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController 
let taskChannel = FlutterMethodChannel(name: "samples.flutter.dev/tasks", 
binaryMessenger: controller.binaryMessenger)

Now we must register the call handler to say we can accept getTasks calls on our new tasks channel and implement what happens when its called:

taskChannel.setMethodCallHandler({ 
  [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in 
  guard call.method == "getTasks" else { 
    result(FlutterMethodNotImplemented) 
    return 
  } 
  self?.receiveTaskRequest(result: result) 
})

Then, decode the arguments and ensure that they meet what we are expecting, then call the desired native SDK function. If the parameters don’t match what we are expecting, then we have to through an error.

private func receiveTaskRequest(request: FlutterMethodCall, result: FlutterResult) { 
  guard let userId = call.arguments as? Int else { 
      // Handle error if no user id passed in as arguments 
      let error = FlutterError(code: "INVALID_PARAM", 
                               message: "No user id passed in" 
                               details: nil)

result(error)
return }

let tasks =

This call chain can be summarised with the following diagram:

The communication channel between flutter and native code

How does this look from a native apps perspective?

iOS communication flow

This gets a users tasks and sets them:import UIKit
import NativeModuleclass TaskViewController: UIViewController {override func viewDidLoad() {
super.viewDidLoad()
let tasks = NativeModule.getTasks(forUserId: 5931)
setTasks(tasks)
}
}

This can be represented using the following diagram for iOS and Android

In conclusion

From this it’s clear to see that Flutter‘s communication channels with native code requires a lot of boiler plate code. These in between interfacing layers are massive obstacles for communication and data to transfer seamlessly. It requires a lot more time and effort to develop, and leaves a lot of extra code that needs maintained over time.

It also means that all models need to be serialisable (able to be converted to/from JSON) which adds additional work on all platforms and comes at a run time performance hit, resulting in a slower app overall.

The lack of static typing means that all of the type checking performed natively is redundant in the front end and communication layers and means that Android, iOS, and Flutter code all need to have the exact same variable names which results in a high chance of run time exceptions occurring.

It’s clear that Flutter should minimise it’s communication with native code as much as possible, and leave this for accessing essential services like platform specific SDK’s. From experience, if you are doing a hybrid app its best to do as much as possible in hybrid code, and use plugins where needed to access native API’s. Only when a task can’t be done using either of them, then you should implement your own communication layer to custom native code.

Subscribe to Aniseed Apps - iOS Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe