【Flutter】Flutter插件开辟之创建iOS端插件

藏宝库编辑 2024-10-7 09:33:10 69 0 来自 中国
创建Flutter插件工程

在Android Studio里点击Flie - New - New Flutter Project,在左侧里选中Flutter,然后点击Next。

1.png

  • 在Project Name里输入项目名,只能是小写英文
  • 在Project type里选择Plugin
  • 在Organization里写包名,.Project Name会拼在包名的最反面成为包名的一部门
也可以使用下令行flutter create --org com.example --template=plugin plugin_name来创建插件,此中com.example就是Organization,plugin_name就是Project Name
点击Finish后就成功创建一个插件工程了。
创建成功后大概默认打开的是Android工程,点击切换为Project。

2.png

切换后可以看到许多文件夹,我们必要关注的重要有以下4个:

  • android目次是用来开辟Android端的插件功能
  • ios目次是用来开辟iOS端的插件功能
  • lib是实现Flutter插件接口的代码
  • example目次是测试项目,用来测试开辟出来的插件的
打开iOS工程

ios目次只是一些零星的文件,是没有工程的,以是我们怎么打开它来编写呢???
我们注意到,在example里也有一个iOS工程,没错,这个才是真正的工程!!!
但是我们打开辟现会报错,也没有插件的文件,那怎么办呢?


在打开工程之前,我们必要在Android Studio里的下令行实行以下下令:
cd exampleflutter run  等实行完成之后,我们就可以打开Runner.xcworkspace文件了,这个时间我们发现,多了一个Pods工程。这个工程着实就是插件工程。
我们在Pods工程的Development Pods目次下,找到Project Name的文件夹,不停睁开,末了就看到插件的文件了,我们就是在这个文件下编写代码。

5.png
编写插件代码

本文采用的语言是Swift。
找到Swift项目名Plugin.swift这个文件,该文件就是插件的实现文件。
在register方法里,我们注册了一个通道(已经默认注册了),通道名默认就是项目名,该名字在通讯里必须是唯一的,可以修改,一旦修改,必要把dart和android里的名字也一并修改。
在handle方法里,实现Flutter调用原生的API,此中call.method就是方法名,call.arguments就是Flutter转达过来的参数。使用result(返回值)可以把效果返回给Flutter。
当找不到方法名时,可以返回FlutterMethodNotImplemented给Flutter表现该方法还没实现,以此来做版本兼容。
具体实现如下:
public class SwiftNakiriPlugin: NSObject, FlutterPlugin {  public static func register(with registrar: FlutterPluginRegistrar) {    let channel = FlutterMethodChannel(name: "nakiri", binaryMessenger: registrar.messenger())    let instance = SwiftNakiriPlugin()    registrar.addMethodCallDelegate(instance, channel: channel)  }  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {      if call.method == "stateString" { // 获取网络状态的实现          result(ZTNetworkStateManager.shared.stateString)      } else if call.method == "bonusPoints" { // 使用参数的实现          let array = call.arguments as! Array<Int>          result(array[0] + array[1])      } else if call.method == "getPlatformVersion" { // 默认的实现          result("iOS " + UIDevice.current.systemVersion)      } else {          // 找不到方法          result(FlutterMethodNotImplemented)      }  }}
Objective-C的插件文件名是项目名Plugin.m,注册方法是registerWithRegistrar,实现插件内容的方法是handleMethodCall
使用第三方库

写插件不可克制的会用到第三方库,在使用第三方库的时间,会遇到3种情况:

  • 仅原生端使用第三方库
  • 仅Flutter端使用第三方库
  • 都使用同一个第三方库
差异的情况有差异的处置惩罚方式。
仅原生端使用第三方库

当仅原生端必要依赖某些第三方库时,可以在项目名.podspec文件里加上s.dependency '第三方库名',如:
s.dependency 'Alamofire'然后打开下令行,跳转到Runner.xcworkspace地点的目次,然后pod install即可。
仅Flutter端使用第三方库

当仅Flutter端必要依赖某些第三方库时,可以在pubspec.yaml文件里的dependencies部门,如:
dependencies:  flutter:    sdk: flutter  url_launcher: ^6.0.16之后在Android Studio里实行Pub get就行了。
都使用同一个第三方库

假设Flutter里必要用到url_launcher,然后原生里也必要用到,那我们就得在Flutter的pubspec.yaml文件里的dependencies部门添加依赖包,同时也要在iOS端的项目名.podspec文件里加上s.dependency 'url_launcher'。
Flutter端实现

刚才我们已经在插件里增加了一个名字叫stateString的方法,但是Flutter端还没实现,我们如今去把它实现。
找到lib文件夹下的项目名.dart文件,内里就有一个类,类名就是项目名,我们增加一个方法用来调用iOS端的stateString方法,方法名不必要和iOS端的保持同等,重要是通道里调用iOS端的方法名就行了,代码如下:
class Nakiri {  static const MethodChannel _channel = MethodChannel('nakiri'); /// 通道名,需和iOS、android端保持同等  /// 默认实现  static Future<String?> get platformVersion async {    final String? version = await _channel.invokeMethod('getPlatformVersion');    return version;  }  /// 实现iOS端新增的方法  static Future<String> stateString() async {    final String state = await _channel.invokeMethod('stateString');    return state;  }  /// 实现iOS端新增的方法  static Future<int> add() async {    final int result = await _channel.invokeMethod('bonusPoints', [5, 8]); /// 吸收一个数组或者字典作为参数转达给原生端    return result;  }}Flutter端测试的实现

在example目次里的lib目次,内里有一个main.dart文件,该文件就是测试使用的文件,我们在它原来的实现上修改一下,代码如下:
class _MyAppState extends State<MyApp> {  String _platformVersion = 'Unknown';  String _stateString = 'Unknown';  int _add = -2;  @override  void initState() {    super.initState();    initPlatformState();  }  Future<void> initPlatformState() async {    String platformVersion;    String stateString;    int add;    try {      platformVersion =          await Nakiri.platformVersion ?? 'Unknown platform version';      stateString =          await Nakiri.stateString();      add =      await Nakiri.add();    } on PlatformException {      platformVersion = 'Failed to get platform version.';      stateString = 'Failed to get stateString';      add = -1;    }    if (!mounted) return;    setState(() {      _platformVersion = platformVersion;      _stateString = stateString;      _add = add;    });  }  @override  Widget build(BuildContext context) {    return MaterialApp(      home: Scaffold(        appBar: AppBar(          title: const Text('Plugin example app'),        ),        body: Column(          mainAxisAlignment: MainAxisAlignment.center,          children: [            Center(              child: Text('Running on: $_platformVersion\n'),            ),            Center(              child: Text('Network is: $_stateString\n'),            ),            Center(              child: Text('Bonus points is: $_add\n'),            ),          ],        ),      ),    );  }}
必要注意的是,Flutter和原生通讯都是异步的,以是都必要使用await和async
6.png 通讯的数据范例

原生与Flutter相互通讯时使用的数据范例是有限定的,以下是可用的数据范例:
DartkotlinJavaObjective-CSwiftnullnullnullNSNullNSNullboolBooleanjava.lang.BooleanNSNumber numberWithBool:NSNumber(value: Bool)或者Boolint 32位平台Intjava.lang.IntegerNSNumber numberWithInt:NSNumber(value: Int32)或者Int32intLongjava.lang.LongNSNumber numberWithLong:NSNumber(value: Int)或者IntdoubleDoublejava.lang.DoubleNSNumber numberWithDouble:NSNumber(value: Double)或者DoubleStringStringjava.lang.StringNSStringString或者NSStringUint8ListByteArraybyte[]FlutterStandardTypedData typedDataWithBytes:FlutterStandardTypedData(bytes: Data)Int32ListIntArrayint[]FlutterStandardTypedData typedDataWithInt32:FlutterStandardTypedData(int32: Data)Int64ListLongArraylong[]FlutterStandardTypedData typedDataWithInt64:FlutterStandardTypedData(int64: Data)Float32ListFloatArrayfloat[]FlutterStandardTypedData typedDataWithFloat32:FlutterStandardTypedData(float32: Data)Float64ListDoubleArraydouble[]FlutterStandardTypedData typedDataWithFloat64:FlutterStandardTypedData(float64: Data)ListListjava.util.ArrayListNSArrayArray或者NSArrayMapHashMapjava.util.HashMapNSDictionaryDictionary或者NSDictionary


  • 从表里可知,Swift的底子范例可以用Objective-C的对象范例,聚集范例可以兼容Objective-C的聚集范例(不外这些都是Swift自己的特性
  • 在使用Swift时,最好还是使用它自己的范例,假如使用Objective-C的范例,就无法判定具体范例,好比Int和Double,在使用Objective-C范例的时间,都是NSNumber
原生与Flutter通讯

上面都是讲Flutter怎么调原生的,那原生能不能主动去调Flutter呢?
我们先看看Flutter提供的3个通讯类:

  • FlutterMethodChannel用于方法调用
  • FlutterBasicMessageChannel用于转达简朴数据
  • FlutterEventChannel用于监听数据流
每种Channel均有三个告急成员变量:

  • name:String范例,代表Channel的名字,也是其唯一标识符
  • messager:BinaryMessenger范例,代表消息信使,是消息的发送与吸收的工具
  • codec: MessageCodec范例或MethodCodec范例,代表消息的编解码器
除了必要自界说name之外,别的变量用默认值即可
前两种Channel都提供了原生和Flutter相互通讯的本领,而FlutterEventChannel不支持Flutter端发送数据,由此可见,它们的应用场所不太一样,接下来我会解说它们每个的使用方法。
全部的Channel都必要名字,在一个项目中大概会有许多的Channel,每个Channel都应该使用唯一的定名标识,否则大概会被覆盖。当有消息从Flutter端发送到原生端时,会根据其转达过来的名字找到该Channel对应的Handler(消息处置惩罚器)
推荐的定名方式是构造名称加插件的名称,比方:com.nakiri.ayame/native_image_view,假如一个插件中包罗了多个Channel可再根据功能模块进一步举行区分
FlutterMethodChannel

通过前面我们已经知道Flutter是通过FlutterMethodChannel去调原生方法的,接下来我们在原生端也使用FlutterMethodChannel去调Flutter的方法。
起首我们改造下原生端的插件类,在内里界说一个属性,同时把register方法注册的FlutterMethodChannel赋值给该属性。
private static var methodChannel: FlutterMethodChannel! methodChannel = FlutterMethodChannel(name: "nakiri", binaryMessenger: registrar.messenger())然后我们就可以通过FlutterMethodChannel的invokeMethod方法调用Flutter的方法了。
// 调用Flutter方法SwiftNakiriPlugin.methodChannel.invokeMethod("updateNumber", arguments: (number + 1)) { value in    result(value) // 获取Flutter方法的返回值,并返回给Flutter}然后在Flutter端插件代码中增加监听方法:
static int number = 1;/// 当必要原生调用Flutter方法时,请先调用下初始化方法来增加监听static void init() {    /// 设置原生调用Flutter时的回调    _channel.setMethodCallHandler((call) async {       switch(call.method) {         case "updateNumber":           return _updateNumber(call.arguments); /// 把效果返回给原生端         default:           break;       }    });}  /// 实现原生调用Flutter方法static int _updateNumber(int value) {    return number + value;}FlutterBasicMessageChannel

相对于FlutterMethodChannel必要绑定署理,FlutterBasicMessageChannel在处置惩罚消息上更为方便机动,而且能发送大内存数据块的数据。
Flutter端使用FlutterBasicMessageChannel发送数据给原生端

流程如下:

  • 原生端创建FlutterBasicMessageChannel
  • 原生端使用setMessageHandler方法,设置该Channel的MessageHandler回调
  • Flutter端创建该name的BasicMessageChannel
  • Flutter端使用该BasicMessageChannel通过send方法向原生端发送消息
  • 原生端的MessageHandler收到Flutter端发送的消息,在闭包中获取到值,而且可以通过reply闭包给Flutter端复兴
  • Flutter端处置惩罚该复兴
原生端的插件类增加的代码:
private static var basicMessageChannel: FlutterBasicMessageChannel!// 原生和Flutter相互发送数据basicMessageChannel = FlutterBasicMessageChannel(name: "flutter_plugin_basic_nakiri", binaryMessenger: registrar.messenger())        // 设置消息吸收器,用来吸收数据;当Flutter端发送消息过来的时间,会主动回调;设置为nil时取消监听basicMessageChannel.setMessageHandler { value, reply in    reply(value as! Int + 1) // 使用reply给Flutter端复兴消息}Flutter端插件只必要增加一个属性即可:
static const BasicMessageChannel basicMessageChannel = BasicMessageChannel("flutter_plugin_basic_nakiri", StandardMessageCodec()); /// 界说一个渠道变乱监听;名字必要唯一且各端保持同等然后在Flutter测试工程里,对原生发送数据:
Nakiri.basicMessageChannel.send(_basicNumber).then((value) {    setState(() {        _basicNumber = value;    });});原生端使用FlutterBasicMessageChannel发送数据给Flutter端

流程如下:

  • Flutter端创建BasicMessageChannel
  • Flutter端使用setMessageHandler方法设置该Channel的Handler回调
  • 原生端创建该name的BasicMessageChannel
  • 原生端使用该BasicMessageChannel通过sendMessage方法向Flutter端发送消息
  • Flutter端的Handler收到发送的消息,并处置惩罚消息,然后通过return举行复兴
  • 原生端处置惩罚该复兴
原生端的插件类增加的代码:
// 给Flutter发送数据,并期待Flutter端复兴SwiftNakiriPlugin.basicMessageChannel.sendMessage(number + 1) { value in    result(value)}Flutter端插件增加的代码:
/// 设置原生发送消息给Flutter时的回调basicMessageChannel.setMessageHandler((message) async {    return message; /// 收到消息后,可以通过return把值复兴给原生});FlutterEventChannel

由于FlutterEventChannel只能原生给Flutter发送消息,而且无返回值,以是它只能用来传输一些实时数据。
使用FlutterEventChannel步调如下:

  • Flutter端界说一个EventChannel
  • Flutter端使用receiveBroadcastStream里的listen监听该Channel
  • 原生端界说一个EventChannel
  • 原生端通过setStreamHandler方法设置署理
  • 原生端实现署理的onListen和onCancel方法,并在onListen里获取eventSink闭包,在onCancel方法里开释eventSink闭包
  • 原生端使用eventSink闭包方法消息
  • Flutter端吸收到消息并处置惩罚
原生端增加一个类,用来封装干系逻辑:
class ZTEventChannel: NSObject {        private var eventChannel: FlutterEventChannel?        private var eventSink: FlutterEventSink?        private var timer: Timer?        private var number = 5        override init() {        super.init()    }        required convenience init(binaryMessenger messenger: FlutterBinaryMessenger) {        self.init()        eventChannel = FlutterEventChannel(name: "flutter_plugin_event_nakiri", binaryMessenger: messenger) // 通道名必须唯一且和各端保持同等        eventChannel?.setStreamHandler(self)    }        private func removeTimer() {        timer?.invalidate()        timer = nil        number = 5    }        private func createTimer() {        if #available(iOS 10.0, *) {            if timer == nil {                timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] (timer) in                    self?.timerCall()                })            }        }    }        @objc private func timerCall() {        if let event = eventSink {            event(number)                        number += 5        }    }}extension ZTEventChannel: FlutterStreamHandler {        func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {        // 在这里获取到eventSink        self.eventSink = events        createTimer()                return nil    }        func onCancel(withArguments arguments: Any?) -> FlutterError? {        // 在这里移除eventSink        self.eventSink = nil        removeTimer()                return nil    }}然后在原生端插件类里使用该类:
// 原生实时发送数据流给FluttereventChannel = ZTEventChannel(binaryMessenger: registrar.messenger())Flutter端插件增加一个属性:
static const EventChannel eventChannel = EventChannel("flutter_plugin_event_nakiri"); /// 界说一个渠道变乱监听;名字必要唯一且各端保持同等然后在别的地方可以使用该属性来吸收原生端发来的数据了:
_initStream() {    /// 监听原生发来的消息    _stream ??= Nakiri.eventChannel.receiveBroadcastStream().listen((data) {        /// 这里的data就是原生端发送过来的数据        setState(() {            _eventValue = data;        });        }, onError: (error) { /// 错误处置惩罚        setState(() {            _eventValue = -5;        });    });}别的,还可以停息吸收数据、重新吸收数据和移除吸收数据:
/// 停息数据吸收_stream?.pause();/// 规复数据流_stream?.resume();_removeStream() {    if (_stream != null) {        /// 移除监听        _stream?.cancel();        _stream = null;    }}
移除监听后,想要重新监听时,只必要调用_initStream()方法即可,不必要重新创建eventChannel的
插件上传到Pub

上传插件前,必要美满一些资料:

  • README.md先容包的文件
  • CHANGELOG.md纪录每个版本中的更改
  • LICENSE包罗软件包允许条款的文件
  • pubspec.yaml的资料
  • 全部公共API的API文档
起首是pubspec.yaml,对Flutter插件来说,pubspec.yaml里除了插件的依赖,还包罗一些元信息,根据必要,把这些补上:
name: xxx # 要发布的项目名称description: xxxxxx. # 项目形貌version: 0.0.1  # 发布的版本homepage: http://www.github.com/xxx  # 项目主页issue_tracker: http://www.github.com/xxx # issue,一样寻常写当前插件源代码的Github issue地点repository: http://www.github.com/xxx.git # 一样寻常写当前插件源代码的Github地点  别的,发布到Pub上的包必要包罗一个LICENSE允许条款文件,不想贫苦的话,可以在GitHub创建堆栈的时间选中一个。
发布前查抄

我们打开下令行,跳转到pubspec.yaml文件地点的目次,在下令利用用以下下令来测试发布:
flutter packages pub publish --dry-run --server=https://pub.dartlang.org  
之以是使用--server来指定服务器,是因为我们在设置情况的时间,一样寻常都设置了这2个变量:PUB_HOSTED_URL=https://pub.flutter-io.cn和FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn,直接上传会出现问题
假如没有发现问题,如图所示:

7.png 然后还必要做的就是上传前的必要整理插件,克制插件过大无法上传:
flutter clean使用以下下令来发布插件:
flutter packages pub publish --server=https://pub.dartlang.org 因为是发布到谷歌的平台,以是必要登录谷歌账号举行认证。
在我们输入flutter packages pub publish下令之后,我们会收到一条认证链接,使用欣赏器打开链接就可以验证了。


我们选择自己的账户,即可开始验证,下令行会自行同步状态,无须我们自己处置惩罚的。


网页出现以下提示,就阐明验证成功。

10.png
之后我们只必要期待即可,下令行会自行上传插件到Pub的。
但是,假如遇到这种情况,阐明是被墙了,必要使用署理。


特殊阐明下,在署理客户端上开启了署理,并未便是下令行就开启了署理,下令行必要额外开启,具体方法可自行查找。
上传成功后,会出现如下提示:


上传成功后,并不会立刻能看到,请耐心期待。
插件的使用方式

插件有4种使用方式:

  • pub
  • git
  • 本地
  • 私有pub库
pub依赖

这种是最常见的方式,直接在工程的pubspec.yaml中写上你必要的插件名和版本,之后实行Pub get就行了。
dependencies:  flutter:    sdk: flutter    nakiri: ^0.0.1 # 添加库git依赖

假如我们不想发布到pub,但又想团队共享插件,那么我们可以把库上传到git堆栈内里,然后在pubspec.yaml中设置,之后实行Pub get就行了。
dependencies:  flutter:    sdk: flutter      nakiri:    git:      url: https://github.com/xxx/nakiri.git      ref: nakiri_fixes_issue_520      path: packages/nakiri_2

  • url:git地点
  • ref:表现git引用,可以是commit hash,tag或者分支
  • path:假如git堆栈中有多个软件包,则可以使用此属性指定软件包
本地依赖

上面的方法都必要上传到服务器,较为贫苦,假如只是自己用或者调试插件,那么最好的方式就是本地依赖,只必要在pubspec.yaml中设置路径,之后实行Pub get就行了。
dependencies:  flutter:    sdk: flutternakiri:    path: ../xxx/nakiri/
path可以是相对路径,也可以是绝对路径
私有pub堆栈依赖

一样寻常而言,pub管理插件比git管理方便,以是一样寻常大公司都会搭建自己的私有pub堆栈,依赖私有pub堆栈也很简朴,只必要在pubspec.yaml中设置完成后,之后实行Pub get就行了。
dependencies:  flutter:    sdk: flutter  nakiri:     hosted:      name: nakiri      url: http://your-package-server.com    version: ^0.0.1依赖覆盖

当2个以上的插件依赖于另一个插件,而且他们所依赖的版本差异等的时间,就大概出现版本辩论,要办理这个辩论,我们可以使用dependency_overrides欺凌某个插件使用某个版本,如:
dependencies:  nakiri_2: ^1.0.1  nakiri: ^0.0.1dependency_overrides:  url_launcher: ^5.4.0这里假设nakiri_2和nakiri都依赖url_launcher,但所依赖的版本不一样,通过这种方式,可以让它们都依赖成5.4.0版本。
固然这种方式可以办理依赖报错,但大概会由于版本的改动使得API接口大概不一样,最终还是大概会出问题,以是,慎用。
干系链接

整个项目已经发到GitHub:demo地点  
插件也已经发布到Pub堆栈:插件地点  
参考资料:Flutter中文网 - Flutter插件教程
iOS OC Swift Flutter开辟群 139322447
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 16:45, Processed in 0.181385 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表