Integrating Flutter and native code (Android and iOS) enables you to use platform-specific APIs and functions that are not immediately accessible via Flutter. This technique entails developing platform-specific code (Kotlin/Java for Android, Swift/Objective-C for iOS) and then executing it from Flutter over platform channels.
Need for Integrating Flutter and Native Code
- Access Platform-Specific Features: Use APIs for Bluetooth, GPS, camera, or battery management that are exclusive to Android or iOS.
- Leverage Existing Native Libraries: Reuse pre-built modules or SDKs, saving time and effort in redevelopment.
- Performance Optimization: Execute performance-critical tasks, such as heavy computations, using native languages for maximum efficiency.
Step-by-Step Approach for Integrating Flutter and Native Code:
1. Setting Up a Platform Channel
A platform channel facilitates communication between Flutter and native code.
Flutter side:
- Define a MethodChannel:
import 'package:flutter/services.dart';
class NativeIntegration {
static const platform = MethodChannel('com.example/native');
// Method to invoke native code
static Future<String> invokeNativeMethod(String method, [dynamic arguments]) async {
try {
final result = await platform.invokeMethod(method, arguments);
return result;
} on PlatformException catch (e) {
print("Failed to invoke: '${e.message}'.");
return "Error: ${e.message}";
}
}
}
- Invoke the Native Method:
// Example usage
void _getBatteryLevel() async {
final batteryLevel = await NativeIntegration.invokeNativeMethod('getBatteryLevel');
print("Battery level: $batteryLevel");
}
Android Side:
- Set Up the MethodChannel in the MainActivity:
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.example/native"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
return batteryLevel
}
}
IOS Side:
- Set Up the MethodChannel in AppDelegate:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "com.example/native",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "getBatteryLevel" {
self.receiveBatteryLevel(result: result)
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == UIDevice.BatteryState.unknown {
result(FlutterError(code: "UNAVAILABLE",
message: "Battery level not available.",
details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}
2. Handling Arguments and Returning Results
- Passing Arguments: You can pass arguments from Flutter to native code by using the invokeMethod function.
final result = await
NativeIntegration.invokeNativeMethod('methodName', {'arg1': 'value1'});
- Returning Results: Native code can send results back to Flutter. This can refer to a simple data type (such as a text or integer) or a more complicated data structure (such as a map).
3. Handling Errors
On the Flutter side, use PlatformException to catch and handle exceptions.
try {
final result = await NativeIntegration.invokeNativeMethod('methodName');
} on PlatformException catch (e) {
print("Failed to invoke: '${e.message}'.");
}
Native Side: Use result.error to return errors to Flutter.
result.error("ERROR_CODE", "Error message", null)
Conclusion
Flutter integration with native code via platform channels is a powerful technique that allows you to leverage platform-specific functionalities and APIs that are not directly accessible through Flutter. By setting up a communication bridge between Flutter and native platforms like Android and iOS, you can invoke native methods, handle arguments, return results, and manage errors efficiently. This approach is beneficial for accessing platform-specific features, interacting with existing native code, and optimizing performance-critical tasks, ensuring your Flutter app can fully utilize the underlying capabilities of the device