This guide describes how to write custom platform-specific code. Some platform-specific functions are available through existing Packages. See For details: Use Packages in Flutter.
Note that the content of this page works on most platforms, But Web plug-ins are generally implemented through either [JS interaction](https://dart.cn/web/js-interop) or [' dart: HTML ' Library](https://api.dart.cn/dart-html/dart-html-library.html) implemented.Copy the code
Flutter uses a flexible system, whether Kotlin or Java on Android, Swift or Objective-C on iOS, that allows you to call platform-specific apis.
Flutter’s built-in platform-specific API support does not rely on any generated code, but flexibly relies on the message format to be delivered. Alternatively, you could use the Pigeon package to generate code to send structured type-safe messages.
If you need to use platform apis or libraries in Java/Kotlin/Objective-C or Swift, this tutorial will use the platform channel mechanism. But you can also do this by checking the Flutter app [defaultTargetPlatform] (https://api.flutter-io.cn/flutter/foundation/defaultTargetPlatform.html) attributes to write related platform Dart code. [differences of different platforms operating experience and adaptation] (https://flutter.cn/docs/resources/platform-adaptations) in the document lists the Flutter framework automatically perform some relevant platform adapter for you.Copy the code
Architecture Overview: Platform channels
Messages are passed between the client (UI) and host (platform) using platform channels, as shown below:
Messages and responses are delivered asynchronously to ensure that the user interface remains responsive.
Flutter sends messages asynchronously through Dart. Even so, when you call a platform method, you need to do the call on the main thread. In [this] (https://flutter.cn/docs/development/platform-integration/platform-channels#channels-and-platform-threading) to view more.Copy the code
MethodChannel responds when the client makes a method call, and from the platform side, MethodChannelAndroid uses MethodChanneliOS on Android and iOS uses MethodChanneliOS to receive and return method calls from MethodChannel. When developing platform plug-ins, you can reduce boilerplate code.
Method calls can also be sent backwards if needed, with the platform acting as a client to invoke the Dart implementation's methods. A concrete example is the [' quick_actions'](https://pub.flutter-io.cn/packages/quick_actions) plug-in.Copy the code
Platform channel data types and codecs
The standard platform channel uses a StandardMessageCodec, which supports simple, efficient binary serialization of json-like values, such as booleans, numbers, strings, byte buffers, and lists and mappings of these types (see StandardMessageCodec for details). As you send and receive values, it automatically serializes and deserializes those values.
The following table shows how to receive the Dart value on the platform side and vice versa:
Dart | Java | Kotlin | Obj-C | Swift |
| -------------------------- | ------------------- | ----------- | ---------------------------------------------- | --------------------------------------- |
| null | null | null | nil (NSNull when nested) | nil |
| bool | java.lang.Boolean | Boolean | NSNumber numberWithBool: | NSNumber(value: Bool) |
| int | java.lang.Integer | Int | NSNumber numberWithInt: | NSNumber(value: Int32) |
| int, if 32 bits not enough | java.lang.Long | Long | NSNumber numberWithLong: | NSNumber(value: Int) |
| double | java.lang.Double | Double | NSNumber numberWithDouble: | NSNumber(value: Double) |
| String | java.lang.String | String | NSString | String |
| Uint8List | byte[] | ByteArray | FlutterStandardTypedData typedDataWithBytes: | FlutterStandardTypedData(bytes: Data) |
| Int32List | int[] | IntArray | FlutterStandardTypedData typedDataWithInt32: | FlutterStandardTypedData(int32: Data) |
| Int64List | long[] | LongArray | FlutterStandardTypedData typedDataWithInt64: | FlutterStandardTypedData(int64: Data) |
| Float32List | float[] | FloatArray | FlutterStandardTypedData typedDataWithFloat32: | FlutterStandardTypedData(float32: Data) |
| Float64List | double[] | DoubleArray | FlutterStandardTypedData typedDataWithFloat64: | FlutterStandardTypedData(float64: Data) |
| List | java.util.ArrayList | List | NSArray | Array |
| Map | java.util.HashMap | HashMap | NSDictionary | Dictionary |
Copy the code
Example: Calling platform iOS and Android code through platform channels
The following code demonstrates how to call the platform-specific API to retrieve and display the current battery level. It calls the BatteryManager API on Android and the Device.BatteryLevel API on iOS via the platform message getBatteryLevel().
This example adds platform-specific code to the main application. If you want to reuse this code for multiple applications, the project creation steps will be slightly different (see Flutter Packages development and submission), but the platform channel code will still be written in the same way.
Note: Complete working code for this example for Android using Java and iOS using Objective-C is available in /examples/platform_channel/. For iOS code implemented with Swift, see /examples/platform_channel_swift/.
Step 1: Create a new application project
First create a new application:
- Running in a terminal:
flutter create batterylevel
By default, our template uses Kotlin for Android or Swift for iOS code. To use Java or Objective-C, use the -i and/or -a flags:
- Running in a terminal:
flutter create -i objc -a java batterylevel
Step 2: Create the Flutter platform client
The application’s State class holds the State of the current application. Expand it to maintain the current battery state.
First, build the channel. Use MethodChannel in a single platform method that returns battery power.
The channel client and host are connected by the channel name passed to the channel constructor. All channel names used in an application must be unique; Domain prefix only to the use of the channel name add prefix, such as: samples. The flutter. Dev/’.
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; . class _MyHomePageState extends State<MyHomePage> { static const platform = MethodChannel('samples.flutter.dev/battery'); // Get battery level. }Copy the code
Next, the method is invoked on the method channel (specifying the specific method invoked through the String identifier getBatteryLevel). The call may fail — for example, if the platform does not support the platform API (such as running in an emulator), so wrap the invokeMethod call in a try-catch statement.
Use the result returned in setState to update the user interface state within _batteryLevel.
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
Copy the code
Finally, replace the build method in the template with a small user interface that displays the battery status as a string with a button to refresh that value.
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
Copy the code
Step 3: Add the Android platform implementation
Start by opening the Android host part of the Flutter app in Android Studio:
- Start the Android Studio
- Select the menu item File > Open…
- Navigate to the directory containing the Flutter application and select the Android folder within it. Click OK.
- Open in project viewjavaUnder folder
MainActivity.java
File.
Next, create a MethodChannel and set up a MethodCallHandler in the configureFlutterEngine() method. Ensure that the channel name used is the same as that used by the Flutter client.
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "samples.flutter.dev/battery";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
// Note: this method is invoked on the main thread.
// TODO
}
);
}
}
Copy the code
Add Android Java code that uses the Android Battery API to retrieve battery power. This code is exactly the same code you would write in a native Android app.
Start by adding the required dependencies to the file header:
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
Copy the code
Then add the following new method below the onCreate() method in the Activity class:
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
Copy the code
Finally, to complete the onMethodCall() method you added earlier, you need to handle the single platform method getBatteryLevel(), so verify it in the call parameter. The platform method is implemented by calling the Android code written in the previous step and using the Result parameter to return a response in the case of success and error. If an unknown method is called, the method is reported.
(call, result) -> {
// Note: this method is invoked on the main thread.
// TODO
}
Copy the code
And replace it with the following:
(call, result) -> {
// Note: this method is invoked on the main thread.
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
Copy the code
You should now be able to run the app on Android. If you are using the Android emulator, set the battery level in the Extension Controls panel, which can be found in the toolbar… Button access.
Step 4A: Add the iOS platform implementation
Start by opening the iOS host section of the Flutter app in Xcode:
- Start the Xcode
- Select the menu item File > Open…
- Navigate to the folder containing the Flutter application and select the ios folder. Click OK.
- Ensure that Xcode project builds without errors.
- Open project navigationRunner > RunnerUnder the
AppDelegate.m
File.
In application didFinishLaunchingWithOptions: method of creating a FlutterMethodChannel and add a handler. Ensure that the channel name used is the same as that used by the Flutter client.
content_copy
#import <Flutter/Flutter.h>
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"samples.flutter.dev/battery"
binaryMessenger:controller.binaryMessenger];
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// Note: this method is invoked on the UI thread.
// TODO
}];
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
Copy the code
Next add iOS Objective-C code that uses the iOS Battery API to retrieve battery power. This code is exactly the same code you would write in a native iOS app.
Add the following method before @end in the AppDelegate class:
content_copy
- (int)getBatteryLevel { UIDevice* device = UIDevice.currentDevice; device.batteryMonitoringEnabled = YES; if (device.batteryState == UIDeviceBatteryStateUnknown) { return -1; } else { return (int)(device.batteryLevel * 100); }}Copy the code
Finally, complete the setMethodCallHandler() method you added earlier. You need to deal with the single platform method getBatteryLevel(), so validate it in the call argument. The platform method is implemented by calling the iOS code you wrote in the previous step and using the Result parameter to return a response in both success and error cases. If an unknown method is called, the method is reported.
content_copy
__weak typeof(self) weakSelf = self; [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { // Note: this method is invoked on the UI thread. if ([@"getBatteryLevel" isEqualToString:call.method]) { int batteryLevel = [weakSelf getBatteryLevel]; if (batteryLevel == -1) { result([FlutterError errorWithCode:@"UNAVAILABLE" message:@"Battery info unavailable" details:nil]); } else { result(@(batteryLevel)); } } else { result(FlutterMethodNotImplemented); } }];Copy the code
You should now be able to run the app on iOS. If you are using the iOS emulator, note that it does not support the Battery API and the app will display “Battery info Unavailable”.
Step 4b: Add the iOS platform implementation using Swift
Note: The following steps are similar to 4a, except that Swift is used instead of Objective-C.
This step assumes that you created the project in the first step using the -i swift option.
You should now be able to run the app on iOS. If you are using the iOS emulator, note that it does not support the Battery API and the app will display “Battery info Unavailable”.
You should now be able to run the app on iOS. If using the iOS Simulator, note that it does not support battery APIs, And the App displays’ Battery Info Unavailable ‘.
A type-safe passage is obtained via Pigeon
In the previous sample, we used MethodChannel to communicate between host and client, but this is not type-safe. For proper communication, calling/receiving messages depends on host and client declaring the same parameters and data types. The Pigeon package can be used as an alternative to MethodChannel, which generates code to send messages in a structured type-safe manner.
In Pigeon, the message interface is defined in Dart, which then generates the corresponding Android and iOS code. For more complex examples and more information, check out Pigeon pub.dev page.
Using Pigeon eliminated the names and data types of messages needed to match strings between hosts and clients. It supports nested classes, message conversion to apis, generating asynchronous wrapping code, and sending messages. The generated code is fairly readable and ensures no conflicts between multiple clients with different versions. Support for Objective-C, Java, Kotlin and Swift (interoperable through Objective-C) languages.
Pigeon sample
Pigeon example
Pigeon file:
content_copy
import 'package:pigeon/pigeon.dart';
class SearchRequest {
String query;
}
class SearchReply {
String result;
}
@HostApi()
abstract class Api {
SearchReply search(SearchRequest request);
}
Copy the code
Dart usage:
content_copy
import 'generated_pigeon.dart' void onClick() async { SearchRequest request = SearchRequest().. query = 'test'; Api api = Api(); SearchReply reply = await api.search(request); print('reply: ${reply.result}'); }Copy the code
Separate platform-specific code from UI code
If you want to use your platform-specific code in multiple Flutter applications, it can be useful to separate the code into platform plug-ins that are located outside the main application directory. See the development and submission of Flutter Packages for details.
Submit platform-specific code as a Package
Share your platform-related code with other developers in the Flutter ecosystem by viewing the Submit Package.
Custom channels and codecs
In addition to the MethodChannel mentioned above, you can also use the more basic BasicMessageChannel, which supports basic asynchronous messaging using custom message codecs. You can also use specialized BinaryCodec, StringCodec, and JSONMessageCodec classes, or create your own codecs.
You can also see examples of custom codecs in the Cloud_firestore plug-in, which serializes and deserializes more types than the default.
Channel and platform threads
When writing code on the platform side, call all channel methods on the platform main thread. On Android, this thread is sometimes called the “main thread,” but technically it is called the UI thread. Annotate the methods needed to run on the UI thread with @uithRead. On iOS, this thread is called the main thread.
Jump to the UI thread in Android
To comply with the requirement of channel-jumping to the Android UI thread, you may need to jump from the background thread to the Android UI thread to execute the channel method. This is done in Android by Posting () a Runnable in an Android UI thread called Looper. This enables Runnable to be executed on the main thread at the next opportunity.
Java code:
content_copy
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
// Call the desired channel message here.
}
});
Copy the code
Kotlin code:
content_copy
Handler(Looper.getMainLooper()).post {
// Call the desired channel message here.
}
Copy the code
Jump to the main thread in iOS
To qualify for channel jump to iOS main thread, you may need to jump from background thread to iOS main thread to execute channel methods. In iOS, this is done by executing a block on the main Dispatch queue:
Objective – C code:
content_copy
dispatch_async(dispatch_get_main_queue(), ^{
// Call the desired channel message here.
});
Copy the code
Swift code:
content_copy
DispatchQueue.main.async {
// Call the desired channel message here.
}
Copy the code
The resources
Write dual-platform code (plug-in writing and implementation)
Writing custom platform-specific code