撰写双端平台代码(插件编写实现)

本指南介绍了如何编写自定义的平台相关代码。某些平台相关功能可通过已有的软件包获得,具体细节可查看:在 Flutter 里使用 Packages

Flutter 使用了灵活的系统,它允许你调用相关平台的 API,无论是 Android 中的 Java 或 Kotlin 代码,还是iOS 中的 Objective-C 或 Swift 代码。

Flutter 的平台相关 API 支持不依赖于代码生成,而是依赖于灵活的消息传递:

  • 应用程序中的 Flutter 部分通过平台通道向其宿主(应用程序中的 iOS 或 Android 部分)发送消息。

  • 宿主监听平台通道并接收消息。然后,它使用原生编程语言来调用任意数量的相关平台API,并将响应发送回客户端(即应用程序中的 Flutter 部分)。

备忘

如果你需要在 Java/Kotlin/Objective-C 或 Swift 中使用平台的 API 或库,本指南将使用平台通道机制。但你也可以通过检查 Flutter 应用程序中的 defaultTargetPlatform 属性来编写相关平台的 Dart 代码。不同平台操作体验的差异和适配 中列出了 Flutter 框架自动为你执行的一些相关平台适配。

架构概述:平台通道

消息使用平台通道在客户端(UI)和宿主(平台)之间传递,如下图所示:

Platform channels architecture

消息和响应以异步的形式进行传递,以确保用户界面能够保持响应。

备忘

Flutter 是通过 Dart 异步发送消息的,即便如此,当你调用一个平台方法时,也需要在主线程上做调用。在 这里查看更多。

客户端 MethodChannelAPI)允许发送与方法调用相对应的消息。平台方面,在Android 的 MethodChannelAPI)及 iOS的 FlutterMethodChannelAPI)上接收方法调用并返回结果。这些类允许你使用非常少的样板代码来开发平台插件。

注意:如果需要,方法调用也可以反向发送,由平台充当客户端来调用 Dart实现的方法。一个具体的例子是 quick_actions插件。

平台通道数据类型及编解码器

标准平台通道使用标准消息编解码器,它支持简单的类似 JSON值的高效二进制序列化,例如布尔值、数字、字符串、字节缓冲区及这些类型的列表和映射(详情请参阅StandardMessageCodec)。当你发送和接收值时,它会自动对这些值进行序列化和反序列化。

下表展示了如何在平台端接收 Dart 值,反之亦然:

DartAndroidiOS
nullnullnil (NSNull when nested)
booljava.lang.BooleanNSNumber numberWithBool:
intjava.lang.IntegerNSNumber numberWithInt:
int, if 32 bits not enoughjava.lang.LongNSNumber numberWithLong:
doublejava.lang.DoubleNSNumber numberWithDouble:
Stringjava.lang.StringNSString
Uint8Listbyte[]FlutterStandardTypedData typedDataWithBytes:
Int32Listint[]FlutterStandardTypedData typedDataWithInt32:
Int64Listlong[]FlutterStandardTypedData typedDataWithInt64:
Float64Listdouble[]FlutterStandardTypedData typedDataWithFloat64:
Listjava.util.ArrayListNSArray
Mapjava.util.HashMapNSDictionary

示例: 通过平台通道调用平台的 iOS 和 Android 代码

以下代码演示了如何调用平台相关 API 来检索并显示当前的电池电量。它通过平台消息 getBatteryLevel()来调用 Android 的 BatteryManager API 及 iOS 的 device.batteryLevel API。

该示例在主应用程序中添加平台相关代码。如果想要将该代码重用于多个应用程序,那么项目的创建步骤将略有差异(查看 Flutter Packages 的开发和提交),但平台通道代码仍以相同方式编写。

注意:可在/examples/platform_channel/中获得使用 Java 实现的 Android 及使用 Objective-C 实现的 iOS 的该示例完整可运行的代码。对于用Swift 实现的 iOS 代码,请参阅/examples/platform_channel_swift/

第一步:创建一个新的应用项目

首先创建一个新的应用:

  • 在终端中运行:flutter create batterylevel

默认情况下,我们的模板使用 Java 编写 Android 或使用 Objective-C 编写 iOS 代码。要使用Kotlin 或 Swift,请使用 -i 和/或 -a 标志:

  • 在终端中运行:flutter create -i swift -a kotlin batterylevel

第二步:创建 Flutter 平台客户端

应用程序的 State 类保持当前应用的状态。扩展它以保持当前的电池状态。

首先,构建通道。在返回电池电量的单一平台方法中使用 MethodChannel

通道的客户端和宿主端通过传递给通道构造函数的通道名称进行连接。一个应用中所使用的所有通道名称必须是唯一的;使用唯一的域前缀为通道名称添加前缀,比如:samples.flutter.dev/battery

  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. ...
  5. class _MyHomePageState extends State<MyHomePage> {
  6. static const platform = const MethodChannel('samples.flutter.dev/battery');
  7. // Get battery level.
  8. }

接下来,在方法通道上调用方法(指定通过 String 标识符 getBatteryLevel调用的具体方法)。调用可能会失败—比如,如果平台不支持此平台API(比如在模拟器中运行),所以将 invokeMethod 调用包裹在 try-catch 语句中。

setState 中使用返回结果来更新 _batteryLevel 内的用户界面状态。

  1. // Get battery level.
  2. String _batteryLevel = 'Unknown battery level.';
  3. Future<void> _getBatteryLevel() async {
  4. String batteryLevel;
  5. try {
  6. final int result = await platform.invokeMethod('getBatteryLevel');
  7. batteryLevel = 'Battery level at $result % .';
  8. } on PlatformException catch (e) {
  9. batteryLevel = "Failed to get battery level: '${e.message}'.";
  10. }
  11. setState(() {
  12. _batteryLevel = batteryLevel;
  13. });
  14. }

最后,将模板中的 build 方法替换为包含以字符串形式显示电池状态、并包含一个用于刷新该值的按钮的小型用户界面。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Material(
  4. child: Center(
  5. child: Column(
  6. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  7. children: [
  8. RaisedButton(
  9. child: Text('Get Battery Level'),
  10. onPressed: _getBatteryLevel,
  11. ),
  12. Text(_batteryLevel),
  13. ],
  14. ),
  15. ),
  16. );
  17. }

步骤 3a: 使用 Java 添加 Android 平台的实现

注意:以下步骤使用 Java。如果你更喜欢 Kotlin,请跳至步骤 3b。

首先在 Android Studio 中打开 Flutter 应用的 Android 宿主部分:

  • 启动 Android Studio

  • 选择菜单项 File > Open…

  • 导航到包含 Flutter 应用的目录,然后选择其中的 android 文件夹。点击 OK

  • 在项目视图中打开 java 文件夹下的 MainActivity.java 文件。

接下来,在 onCreate() 方法中创建一个 MethodChannel 并设置一个MethodCallHandler。确保使用的通道名称与 Flutter 客户端使用的一致。

  1. import io.flutter.app.FlutterActivity;
  2. import io.flutter.plugin.common.MethodCall;
  3. import io.flutter.plugin.common.MethodChannel;
  4. import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
  5. import io.flutter.plugin.common.MethodChannel.Result;
  6. public class MainActivity extends FlutterActivity {
  7. private static final String CHANNEL = "samples.flutter.dev/battery";
  8. @Override
  9. public void onCreate(Bundle savedInstanceState) {
  10. super.onCreate(savedInstanceState);
  11. GeneratedPluginRegistrant.registerWith(this);
  12. new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
  13. new MethodCallHandler() {
  14. @Override
  15. public void onMethodCall(MethodCall call, Result result) {
  16. // Note: this method is invoked on the main thread.
  17. // TODO
  18. }
  19. });
  20. }
  21. }

添加使用 Android battery API 来检索电池电量的 Android Java 代码。该代码与你在原生Android 应用中编写的代码完全相同。

首先在文件头部添加所需的依赖:

  1. import android.content.ContextWrapper;
  2. import android.content.Intent;
  3. import android.content.IntentFilter;
  4. import android.os.BatteryManager;
  5. import android.os.Build.VERSION;
  6. import android.os.Build.VERSION_CODES;
  7. import android.os.Bundle;

然后在 Activity 类中的 onCreate() 方法下方添加以下新方法:

  1. private int getBatteryLevel() {
  2. int batteryLevel = -1;
  3. if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
  4. BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
  5. batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
  6. } else {
  7. Intent intent = new ContextWrapper(getApplicationContext()).
  8. registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
  9. batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
  10. intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
  11. }
  12. return batteryLevel;
  13. }

最后,完成前面添加的 onMethodCall() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数call 中对其进行验证。该平台方法的实现是调用上一步编写的 Android 代码,并使用 result参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。

移除以下代码:

  1. public void onMethodCall(MethodCall call, Result result) {
  2. // TODO
  3. }

并替换成以下内容:

  1. @Override
  2. public void onMethodCall(MethodCall call, Result result) {
  3. // Note: this method is invoked on the main thread.
  4. if (call.method.equals("getBatteryLevel")) {
  5. int batteryLevel = getBatteryLevel();
  6. if (batteryLevel != -1) {
  7. result.success(batteryLevel);
  8. } else {
  9. result.error("UNAVAILABLE", "Battery level not available.", null);
  10. }
  11. } else {
  12. result.notImplemented();
  13. }
  14. }

现在你应该可以在 Android 中运行该应用。如果使用了 Android模拟器,请在扩展控件面板中设置电池电量,可从工具栏中的 按钮访问。

步骤 3b:使用 kotlin 添加 Android 平台的实现

注意:以下步骤与 3a 类似,唯一的区别是使用了 Kotlin 而非 Java。

此步骤假设你在 第一步 中使用 -a kotlin 选项创建了项目。

首先在 Android Studio 中打开 Flutter 应用的 Android 宿主部分:

  • 启动 Android Studio

  • 选择菜单项 File > Open…

  • 导航到包含 Flutter 应用的目录,然后选择其中的 android 文件夹。点击 OK

  • 在项目视图中打开 kotlin 文件夹下的 MainActivity.kt 文件(注意:如果使用Android Studio 2.3 进行编辑,请注意 kotlin 目录的显示名称为 java)。

onCreate() 方法中创建一个 MethodChannel 并调用setMethodCallHandler()。确保使用的通道名称与 Flutter 客户端使用的一致。

  1. import android.os.Bundle
  2. import io.flutter.app.FlutterActivity
  3. import io.flutter.plugin.common.MethodChannel
  4. class MainActivity() : FlutterActivity() {
  5. private val CHANNEL = "samples.flutter.dev/battery"
  6. override fun onCreate(savedInstanceState: Bundle?) {
  7. super.onCreate(savedInstanceState)
  8. GeneratedPluginRegistrant.registerWith(this)
  9. MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
  10. // Note: this method is invoked on the main thread.
  11. // TODO
  12. }
  13. }
  14. }

添加使用 Android battery API 来检索电池电量的 Android Kotlin 代码。该代码与你在原生Android 应用中编写的代码完全相同。

首先在文件头部添加所需的依赖:

  1. import android.content.Context
  2. import android.content.ContextWrapper
  3. import android.content.Intent
  4. import android.content.IntentFilter
  5. import android.os.BatteryManager
  6. import android.os.Build.VERSION
  7. import android.os.Build.VERSION_CODES

然后在 MainActivity 类中的 onCreate() 方法下方添加以下新方法:

  1. private fun getBatteryLevel(): Int {
  2. val batteryLevel: Int
  3. if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
  4. val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
  5. batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
  6. } else {
  7. val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
  8. batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
  9. }
  10. return batteryLevel
  11. }

最后,完成前面添加的 onMethodCall() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数call 中对其进行验证。该平台方法的实现是调用上一步编写的 Android 代码,并使用 result参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。

删除以下代码:

  1. MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
  2. // TODO
  3. }

并替换成以下内容:

  1. MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
  2. // Note: this method is invoked on the main thread.
  3. if (call.method == "getBatteryLevel") {
  4. val batteryLevel = getBatteryLevel()
  5. if (batteryLevel != -1) {
  6. result.success(batteryLevel)
  7. } else {
  8. result.error("UNAVAILABLE", "Battery level not available.", null)
  9. }
  10. } else {
  11. result.notImplemented()
  12. }
  13. }

现在你应该可以在 Android 中运行该应用。如果使用了 Android模拟器,请在扩展控件面板中设置电池电量,可从工具栏中的 按钮访问。

步骤 4a:使用 Objective-C 添加 iOS 平台的实现

注意:以下步骤使用 Objective-C,如果你更喜欢 Swift,请跳至步骤 4b。

首先在 Xcode 中打开 Flutter 应用的 iOS 宿主部分:

  • 启动 Xcode

  • 选择菜单项 File > Open…

  • 导航到包含 Flutter 应用的目录,然后选择其中的 ios 文件夹。点击 OK

  • 确保 Xcode 项目构建没有错误。

  • 打开项目导航 Runner > Runner 下的 AppDelegate.m 文件。

application didFinishLaunchingWithOptions: 方法中创建一个 FlutterMethodChannel并添加一个处理程序。确保使用的通道名称与 Flutter 客户端使用的一致。

  1. #import <Flutter/Flutter.h>
  2. #import "GeneratedPluginRegistrant.h"
  3. @implementation AppDelegate
  4. - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  5. FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
  6. FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
  7. methodChannelWithName:@"samples.flutter.dev/battery"
  8. binaryMessenger:controller];
  9. [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  10. // Note: this method is invoked on the UI thread.
  11. // TODO
  12. }];
  13. [GeneratedPluginRegistrant registerWithRegistry:self];
  14. return [super application:application didFinishLaunchingWithOptions:launchOptions];
  15. }

接下来添加使用 iOS battery API 来检索电池电量的 iOS Objective-C 代码。该代码与你在原生iOS 应用中编写的代码完全相同。

AppDelegate 类中的 @end 之前添加以下方法:

  1. - (int)getBatteryLevel {
  2. UIDevice* device = UIDevice.currentDevice;
  3. device.batteryMonitoringEnabled = YES;
  4. if (device.batteryState == UIDeviceBatteryStateUnknown) {
  5. return -1;
  6. } else {
  7. return (int)(device.batteryLevel * 100);
  8. }
  9. }

最后,完成前面添加的 setMethodCallHandler() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数call 中对其进行验证。该平台方法的实现是调用上一步编写的 iOS 代码,并使用 result参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。

  1. __weak typeof(self) weakSelf = self;
  2. [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  3. // Note: this method is invoked on the UI thread.
  4. if ([@"getBatteryLevel" isEqualToString:call.method]) {
  5. int batteryLevel = [weakSelf getBatteryLevel];
  6. if (batteryLevel == -1) {
  7. result([FlutterError errorWithCode:@"UNAVAILABLE"
  8. message:@"Battery info unavailable"
  9. details:nil]);
  10. } else {
  11. result(@(batteryLevel));
  12. }
  13. } else {
  14. result(FlutterMethodNotImplemented);
  15. }
  16. }];

现在你应该可以在 iOS 中运行该应用。如果使用了 iOS 模拟器,注意它并不支持battery API,并且应用会显示 ‘battery info unavailable’。

步骤 4b:使用 Swift 添加 iOS 平台的实现

注意:以下步骤与 4a 类似,唯一的区别是使用了 Swift 而非 Objective-C。

此步骤假设你在第一步中使用 -i swift 选项创建了项目。

首先在 Xcode 中打开 Flutter 应用的 iOS 宿主部分:

  • 启动 Xcode

  • 选择菜单项 File > Open…

  • 导航到包含 Flutter 应用的目录,然后选择其中的 ios 文件夹。点击 OK

在使用 Objective-C 的标准模板设置中添加对 Swift 的支持:

  • 在项目导航中展开 Expand Runner > Runner

  • 打开项目导航 Runner > Runner 下的 AppDelegate.swift 文件。

重写 application:didFinishLaunchingWithOptions: 方法并创建一个绑定了通道名称samples.flutter.dev/batteryFlutterMethodChannel

  1. @UIApplicationMain
  2. @objc class AppDelegate: FlutterAppDelegate {
  3. override func application(
  4. _ application: UIApplication,
  5. didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  6. let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
  7. let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
  8. binaryMessenger: controller.binaryMessenger)
  9. batteryChannel.setMethodCallHandler({
  10. (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
  11. // Note: this method is invoked on the UI thread.
  12. // Handle battery messages.
  13. })
  14. GeneratedPluginRegistrant.register(with: self)
  15. return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  16. }
  17. }

接下来添加使用 iOS battery API 来检索电池电量的 iOS Swift 代码。该代码与你在原生iOS 应用中编写的代码完全相同。

AppDelegate.swift 末尾添加以下新方法:

  1. private func receiveBatteryLevel(result: FlutterResult) {
  2. let device = UIDevice.current
  3. device.isBatteryMonitoringEnabled = true
  4. if device.batteryState == UIDevice.BatteryState.unknown {
  5. result(FlutterError(code: "UNAVAILABLE",
  6. message: "Battery info unavailable",
  7. details: nil))
  8. } else {
  9. result(Int(device.batteryLevel * 100))
  10. }
  11. }

最后,完成前面添加的 setMethodCallHandler() 方法。你需要处理单个平台方法 getBatteryLevel(),所以在参数call 中对其进行验证。该平台方法的实现是调用上一步编写的 iOS 代码。如果调用了未知方法,则报告该方法。

  1. batteryChannel.setMethodCallHandler({
  2. [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
  3. // Note: this method is invoked on the UI thread.
  4. guard call.method == "getBatteryLevel" else {
  5. result(FlutterMethodNotImplemented)
  6. return
  7. }
  8. self?.receiveBatteryLevel(result: result)
  9. })

现在你应该可以在 iOS 中运行该应用。如果使用了 iOS 模拟器,注意它并不支持battery API,并且应用会显示 ‘battery info unavailable’。

从 UI 代码中分离平台相关代码

如果你想要在多个 Flutter 应用中使用你的平台相关代码,则将代码分离为位于主应用目录之外的平台插件会很有用。相关细节查看Flutter Packages 的开发和提交

将平台相关代码作为 Package 进行提交

与 Flutter 生态中的其他开发者共享你的平台相关代码,可查看 提交 package

自定义通道和编解码器

除了上面提到的 MethodChannel,你还可以使用更基础的BasicMessageChannel,它支持使用自定义的消息编解码器进行基本的异步消息传递。你还可以使用专门的BinaryCodecStringCodecJSONMessageCodec 类,或创建自己的编解码器。

Channels and Platform Threading

Invoke all channel methods on the platform’s main thread when writing code onthe platform side. On Android, this thread is sometimes called the “mainthread”, but it is technically defined as the UI thread. Annotate methods thatneed to be run on the UI thread with @UiThread. On iOS, this thread isofficially referred to as the main thread.

Jumping to the UI thread in Android

To comply with channels’ UI thread requirement, you may need to jump from abackground thread to Android’s UI thread to execute a channel method. InAndroid this is accomplished by post()ing a Runnable to Android’s UIthread Looper, which will cause the Runnable to execute on the main threadat the next opportunity.

In Java:

  1. new Handler(Looper.getMainLooper()).post(new Runnable() {
  2. @Override
  3. public void run() {
  4. // Call the desired channel message here.
  5. }
  6. });

In Kotlin:

  1. Handler(Looper.getMainLooper()).post {
  2. // Call the desired channel message here.
  3. }

Jumping to the main thread in iOS

To comply with channel’s main thread requirement, you may need to jump from abackground thread to iOS’s main thread to execute a channel method. In iOS thisis accomplished by executing a block on the main dispatch queue:

In Objective-C:

  1. dispatch_async(dispatch_get_main_queue(), ^{
  2. // Call the desired channel message here.
  3. });

In Swift:

  1. DispatchQueue.main.async {
  2. // Call the desired channel message here.
  3. }