Flutter第三部分(平台整合):Flutter中特定平台代码的编写

前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者搜索”IT工匠“关注微信公众号/头条号(微信公众号和头条号同名),会同步推送)。

本文主要介绍如何编写平台特定的代码,Flutter使用了一套灵活的系统以保证我们可以调用特定平台的API,这里的**"特定平台的API”**可以是由Android上的JavaKotlin代码构建的,也可以是iOS上由ObjectiveCSwift代码构建的。

Flutter对特定平台API的调用是基于这样一套流程的:

  • Flutter代码通过平台通道(platform channel)将消息发送到原生代码层即我们的特定平台的代码层(iOSAndroid)。
  • iOS或者Android层的原生代码通过对平台通道的监听,在合适的时机接收Flutter代码发送的消息。
  • iOS或者Android层的原生代码根据接收到的消息调用不同的本平台的原生API
  • iOS或者Android层的原生代码将原生API的执行结果返回给Flutter层的代码

整个过程的示意图是这样的:

Flutter第三部分(平台整合):Flutter中特定平台代码的编写_第1张图片

注意:如果你需要在Java/Kotlin/Objective-C/Swift中使用特定平台独有的API或库,可以通过本文的内容进行实现,但是,如果你只是想根据不同的平台执行不同的代码,则不需要使用本文的办法编写平台特定的代码,只需要在Flutter应用程序通过检查defaultTargetPlatform属性的值来确定当前程序运行的平台,然后根据当前平台调用不同的Dart代码即可。

框架概述:平台通道

这里我们首先规定两个术语,我们将Flutter层的代码称为客户端代码,将AndroidiOS层的平台特定代码称为宿主端代码。

使用平台通道在客户端(Flutter UI)和宿主(AndroidiOS平台)之间传递消息的原理示意图如下:

Flutter第三部分(平台整合):Flutter中特定平台代码的编写_第2张图片

发送消息和等待响应都是异步的过程,这样可以保证不会造成我们用户界面的阻塞。

在客户端(Flutter层),MethodChannel API可以发送与方法调用相对应的消息。

在宿主平台上(AndroidiOS层),Android上的MethodChannel API和iOS上的FlutterMethodChannel API 可以通过接收从平台通道传递过来的方法调用请求从而进行对应的方法的调用,最后将调用结果通过平台通道返回给客户端。

注意: 如果需要,特定平台也可以反过来调用Flutter层的API

平台通道支持的数据类型和解码器

标准平台通道使用标准消息编解码器,以支持简单的类似JSON值的高效二进制序列化,例如 booleans,numbers, Strings, byte buffers, List, Maps等, 当你通过平台通道进行消息的发送和接收时,被发送的消息会自动进行序列化(发送时)和反序列化(接收时)。

下表列出了Dart语言中的数据类型在AndroidiOS的对应类型:

Dart Android iOS
null null nil (NSNull when nested)
bool java.lang.Boolean NSNumber numberWithBool:
int java.lang.Integer NSNumber numberWithInt:
int, 如果32位int不够用 java.lang.Long NSNumber numberWithLong:
int, 如果64位int不够用 java.math.BigInteger FlutterStandardBigInteger
double java.lang.Double NSNumber numberWithDouble:
String java.lang.String NSString
Uint8List byte[] FlutterStandardTypedData typedDataWithBytes:
Int32List int[] FlutterStandardTypedData typedDataWithInt32:
Int64List long[] FlutterStandardTypedData typedDataWithInt64:
Float64List double[] FlutterStandardTypedData typedDataWithFloat64:
List java.util.ArrayList NSArray
Map java.util.HashMap NSDictionary

实例: 使用平台通道获取设备当前电量

下面我们来通过一个实例展示如何在Flutter中实现特定平台API的调用,我们要实现的功能是通过调用平台特定的API来获取和显示当前设备的电池电量。实现的方法是通过调用Android端的BatteryManager APIiOS端的 device.batteryLevel API 进行当前设备电量的获取。

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

首先创建一个新的应用程序:

  • 在终端运行中:flutter create batterylevel

默认情况下,模板支持使用Java编写Android代码,或使用Objective-C编写iOS代码。要使用KotlinSwift,请使用-i和/或-a标志:

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

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

应用的State类拥有当前的应用状态,我们需要继承这个类以托管当前设备的电量。

首先,我们构造一个通道(MethodChannel),在构造通道的时候需要传入一个通道名称,这个通道名称是客户端和宿主端进行连接的纽带(或者理解为密匙),我们所指定的通道名称必须在本app内是全局唯一的,Flutter官方的建议是在通道名称前加一个唯一的“域名前缀”以避免通道名称的冲突,例如flutter_demo_code.channel_demo.chennel/battery

class _MyAppState extends State {
  static const platform =
      const MethodChannel('flutter_demo_code.channel_demo.chennel/battery');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('IT工匠'),
        ),
        body: Center(
          child: Text('demo'),
        ),
      ),
    );
  }
}

构件好通道(MethodChannel)之后,我们就可以通过构建好的通道调用特定平台的方法了,这里使用的是MethodChannel.invokeMethod(String method, [ dynamic arguments ]) 方法,该方法接收两个参数:

  • methodString类型的参数,表示特定平台上被调用的方法名称
  • [ dynamic arguments ]:一个数组,表示的是调用特定平台上方法时传入的参数

以我们这个例子来说,我们稍后会在Android端和iOS端分别写一个int getBatteryLevel()方法,这个方法的作用是获取当前设备的电量并返回,那么这里的method就应该传入getatteryLevel这个字符串,注意没有(),而如果还需要传递参数,直接构造一个[ dynamic arguments]类型的参数并传递进去即可,我们来看我们的代码实现(注意看注释):

	String _batteryLevel = '电池电量未知';

  Future _getBatteryLevel() async {
    String batteryLevel;
    /**
    这里使用了一个try…catch将 platform.invokeMethod()方法包裹了起来,原因是该方法的调用是有可能失败的,或者说是有可能抛出异常的,比如当平台不支持平台API(例如在模拟器中运行)时。
    */
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = '当前电量:$result % .';
    } on PlatformException catch (e) {
      batteryLevel = "获取电池电量失败,失败原因: '${e.message}'.";
    }

    //使用返回的结果,在setState中来更新用户界面状态batteryLevel。
    setState(() {
      _batteryLevel = batteryLevel;
    });

最后,我们在build()中分别添加一个用于显示当前电池电量的Text和用于更新当前电池电量的RaisedButton

@override
Widget build(BuildContext context) {
  return new Material(
    child: new Center(
      child: new Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          new RaisedButton(
            child: new Text('获取当前设备电量'),
            onPressed: _getBatteryLevel,
          ),
          new Text(_batteryLevel),
        ],
      ),
    ),
  );
}

第三步: 使用Java添加Android平台的特定实现

首先在Android Studio中打开您的Flutter应用的Android部分(当然,如果你习惯直接在Flutter项目中修改iOS部分也是没问题的):

  1. 启动 Android Studio
  2. 选择 ‘File > Open…’
  3. 定位到你的 Flutter app目录, 然后选择里面的 android文件夹,点击 OK
  4. java目录下打开 MainActivity.java

接下来,在onCreate()里做两件事:

  • 构造一个管道(MethodChannel)的实例,在构造管道的时候传入我们刚才在Flutter平台上指定的管道名称flutter_demo_code.channel_demo.chennel/battery
  • 为构造好的管道(MethodChannel)类设置MethodCallHandler,相当于对管道进行监听,当Flutter调用本管道的方法时,会在MethodCallHandleronMethodCall()方法收到回调

具体实现代码如下:

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

public class MainActivity extends FlutterActivity {
     private static final String CHANNEL = "flutter_demo_code.channel_demo.chennel/battery";

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
                new MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, Result result) {
                        // TODO
                    }
                });
    }
}

然后我们使用Java代码,基于Android电池API来获取当前设备的电量:

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;
}

最后,我们完成之前添加的onMethodCall()方法。我们需要处理管道中方法名为getBatteryLevel的调用,所以我们对call参数进行检查,看当前call参数中的方法名是否为getBatteryLevel,如果是,则调用上一步编写的getBatteryLevel()方法并将结果通过result返回给Flutter层:

@Override
public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("getBatteryLevel")) {
        int batteryLevel = getBatteryLevel();
        if (batteryLevel != -1) {
            result.success(batteryLevel);
        } else {
            //如果出现错误调用这个方法,Flutter层会抛出PlatformException异常
            result.error("UNAVAILABLE", "Battery level not available.", null);
        }
    } else {
      //Flutter层会抛出MissingPluginException异常
        result.notImplemented();
    }
}               

现我们就可以在Android设备上运行该Flutter程序,运行效果是这样的:

Flutter第三部分(平台整合):Flutter中特定平台代码的编写_第3张图片

需要说明的是,本小节在Android端使用的是Java语言,如果你想使用Kotlin,也是完全可以的,逻辑完全一样,只是代码语法不同而已。

第四步: 使用Objective-C添加iOS平台的特定实现

使用Xcode打开Flutter应用程序中的iOS部分(当然,如果你习惯直接在Flutter项目中修改iOS部分也是没问题的):

  1. 启动 Xcode
  2. 选择 ‘File > Open…’
  3. 定位到你的 Flutter app目录, 然后选择里面的 iOS文件夹,点击 OK
  4. 确保Xcode项目的构建没有错误。
  5. 选择 Runner > Runner ,打开AppDelegate.m

接下来,进行两步操作:

  • application didFinishLaunchingWithOptions:方法内部创建一个管道(FlutterMethodChannel),确保管道名称为flutter_demo_code.channel_demo.chennel/battery
  • 为创建好的管道添加一个处理方法

实现代码如下:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  // Override point for customization after application launch.
    FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
    
    FlutterMethodChannel* batteryChannel = [FlutterMethodChannel methodChannelWithName:@"flutter_demo_code.channel_demo.chennel/battery"
                                            binaryMessenger:controller];
    
    [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
        //TODO
    }];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

然后,我们基于ObjectiveC代码,通过iOS电池API来获取电池电量:

- (int)getBatteryLevel {
  UIDevice* device = UIDevice.currentDevice;
  device.batteryMonitoringEnabled = YES;
  if (device.batteryState == UIDeviceBatteryStateUnknown) {
    return -1;
  } else {
    return (int)(device.batteryLevel * 100);
  }
}

最后,我们完成之前添加的setMethodCallHandler方法,对平台方法名为getBatteryLevel的管道调用进行捕捉和处理,将处理的返回结果返回给Flutter层:

[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  if ([@"getBatteryLevel" isEqualToString:call.method]) {
    int batteryLevel = [self getBatteryLevel];

    if (batteryLevel == -1) {
      result([FlutterError errorWithCode:@"UNAVAILABLE"
                                 message:@"Battery info unavailable"
                                 details:nil]);
    } else {
      result(@(batteryLevel));
    }
  } else {
    result(FlutterMethodNotImplemented);
  }
}];

现我们就可以在Android设备上运行该Flutter程序了,由于贫穷限制了笔者,所以此处放不出真机演示图,大家自行脑补。

需要说明的是,本小节在iOS端使用的是Objective-C语言,如果你想使用Swift,也是完全可以的,逻辑完全一样,只是代码语法不同而已。

从UI代码中分离平台特定的代码

如果你希望在你的平台特定代码可以用在多个Flutter程序中,那么将这部分抽取出来做成一个packages会十分有效,关于packages部分我会在明天进行介绍,欢迎关注。

将平台特定的代码作为一个包发布

如果您希望与Flutter生态系统中的其他开发人员分享你的特定平台代码,可以将需要共享的部分抽取出来做成一个插件进行发布,关于这部分我也会在明天进行介绍,欢迎关注。

你可能感兴趣的:(Flutter)