likes
comments
collection
share

Flutter插件开发与发布

作者站长头像
站长
· 阅读数 32

一、概述

几乎所有主流的开发语言都有自己的包管理工具。Node开发有npm、Android开发有Gradle,iOS使用CocoaPods作为主要的包管理工具,Flutter也有自己的Dart Packages仓库。插件的开发和复用能够提高开发效率,降低工程的耦合度,像网络请求(http)、用户授权(permission_handler)等客户端开发常用的功能模块,我们只需要引入对应插件就可以为项目快速集成相关能力,从而专注于具体业务功能的实现。

二、为什么要开发 Flutter 插件

除了使用仓库中的流行组件以外,在Flutter项目开发过程中面对通用业务逻辑拆分、或者需要对原生能力封装等场景时,开发者仍然需要开发新的组件。

Flutter 插件化开发的好处:

  • 组件独立维护,降低工程耦合
  • 降低开发Flutter新模块的成本
  • 保持整体风格统一

三、Flutter与Native通信

在Flutter插件开发过程中,几乎都会需要进行Flutter与Native端的数据交互,因此在进行插件开发之前,我们先简单了解下Platform Channel机制

Flutter与Native的通信是通过Platform Channel实现的,它是一种C/S模型,其中Flutter作为Client,iOS和Android平台作为Host,Flutter通过该机制向Native发送消息,Native在收到消息后调用平台自身的API进行实现,然后将处理结果再返回给Flutter页面。

四、插件创建

Flutter组件根据是否包含原生代码可分为两种:

  • Flutter Package(包) :仅包含dart代码,一般是对flutter特定功能的封装实现,例如用于网络请求的http包。

  • Flutter Plugin(插件) :除了dart代码之外,还包含了Android和iOS平台的代码实现,常用于将客户端原生的能力进行封装,然后提供给flutter项目使用。例如用于判断键盘可见状态的flutter_keyboard_visibility插件,就是分别在iOS和Android端监听了键盘的打开和关闭事件,然后将对应事件通过Platform Channel传递给Flutter项目。

1. 创建Dart包

使用--template=package声明创建的是只包含dart代码的package。

flutter create --template=package hello

lib目录用于存放package的代码实现,Flutter脚手架会自动生成一个与package同名的dart文件。

pubspec.yaml文件是dart 生态下的包管理配置文件类似 Android 中的 gradle、iOS 中的 Podfile,在这里可以统一管理整个 flutter 工程的 dart 依赖包,以及管理整个插件的发布属性。

2. 创建Flutter插件

  • 使用--template=plugin声明创建的是同时包含了iOS和Android代码的plugin;
  • 使用--org选项指定组织,一般采用反向域名表示法;
  • 使用-i选项指定iOS平台开发语言,objc或者swift;
  • 使用-a选项指定Android平台开发语言,java或者kotlin。

flutter create --template=plugin --org com.xxxx.xxx -i objc -a java  native_image_view

相比Package,Plugin多出了一些目录,android目录用于Android平台的代码实现,ios目录用于iOS平台的代码实现,example目录用于该组件的调试。

五、插件开发

Plugin和Package的开发和发布流程基本一致,相比之下Plugin还涉及到iOS和Android的开发,实现起来要更加复杂一些。

在Flutter嵌入原生项目的场景中,比较常见的一个问题是:Flutter和原生项目中都使用了同一张图片时,两侧会分别进行存储,即该图片会被存储两次。不同于Weex、Hippy等基于JS的跨平台框架是依赖于原生进行图片的获取和显示,Flutter是自行进行图片的管理并直接通过Skia引擎直接进行绘制的。

针对这一问题,开发一个Flutter插件(native_image_view),把Flutter图片的下载和缓存工作交给Native实现,Flutter端则仅负责图片的绘制。此外,我们还可以定义一个特殊协议,用于处理本地图片的调用,同时解决Flutter无法复用原生项目本地图片的问题。

1. Flutter端开发


我们首先在Flutter端声明了插件的MethodChannel,然后在initState方法中通过invokeMethod(方法名,参数)发起了对Native端的方法调用,在build方法中先显示图片的打底图,待图片数据返回后再调用setState,使用Image.memory方法将二进制数据绘制成图片显示。

native_image_view.dart:

class _NativeImageViewState extends State<NativeImageView> {

  Uint8List _data;

  static const MethodChannel _channel =

      const MethodChannel('com.tencent.game/native_image_view');

  @override

  void initState() {

    super.initState();

    loadImageData();

  }

  


  loadImageData() async {

    _data = await _channel.invokeMethod("getImage", {"url": widget.url});

    setState(() {});

  }

  


  @override

  Widget build(BuildContext context) {

    return _data == null

        ? Container(

            color: Colors.grey,

            width: widget.width,

            height: widget.height,

          )

        : Image.memory(

            _data,

            width: widget.width,

            height: widget.height,

            fit: BoxFit.cover,

          );

  }

}

2. Native端开发

(1)iOS开发

插件的iOS平台使用SDWebImage组件进行网络图片的下载和缓存,因此在native_image_view.podspec文件中声明依赖。

s.dependency 'Flutter'

s.dependency 'SDWebImage'

s.platform = :ios, ’12.0

Flutter脚手架自动为我们生成了NativeImageViewPlugin.m文件和registerWithRegistrar方法,该方法是组件执行的入口,会被Flutter的插件管理器自动调用。

我们在该方法中使用与Flutter端相同的name创建MethodChannel,并创建插件对象的实例,用于处理Flutter端的方法调用。handleMethodCall方法会在MethodChannel收到Flutter端的方法调用后被触发,开发者可以通过FlutterMethodCall获取方法名和参数,通过FlutterResult返回图片内容。

NativeImageViewPlugin.m:

//插件注册接口,Flutter自动调用

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {

  FlutterMethodChannel* channel = [FlutterMethodChannel

      methodChannelWithName:@"com.tencent.game/native_image_view"

            binaryMessenger:[registrar messenger]];

  NativeImageViewPlugin* instance = [[NativeImageViewPlugin alloc] init];

  [registrar addMethodCallDelegate:instance channel:channel];

}

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {

  if ([@"getImage" isEqualToString:call.method]) {

      [self getImageHandler:call result:result];

  } else {

      result(FlutterMethodNotImplemented);

  }

}

在处理Flutter端发起的图片调用时,首先判断Flutter请求的是本地还是网络图片,如果是本地图片则直接根据UIImage对象读取图片的二进制数据返回;如果是网络图片则先判断是否存在本地缓存,有缓存直接返回,无缓存则需要先下载图片然后再返回数据。

- (void)getImageHandler:(FlutterMethodCall*)call result:(FlutterResult)result{

  if(call.arguments != nil && call.arguments[@"url"] != nil){

      NSString *url = call.arguments[@"url"];

      if([url hasPrefix:@"localImage://"]){

        //获取本地图片

        NSString *imageName = [url stringByReplacingOccurrencesOfString:@"localImage://" withString:@""];

        UIImage *image = [UIImage imageNamed:imageName];

        if(image != nil){

            NSData *imgData = UIImageJPEGRepresentation(image,1.0);

            result(imgData);

        }else{

            result(nil);

        }

      }else {

        //获取网络图片

        UIImage *image = [[SDImageCache sharedImageCache] imageFromCacheForKey:url];

        if(!image){

          //本地无缓存,下载后返回图片

          [[SDWebImageDownloader sharedDownloader] 

            downloadImageWithURL:[[NSURL alloc] initWithString:url] 

            completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {

              if(finished){

                result(data);

                [[SDImageCache sharedImageCache] storeImage:image forKey:url completion:nil];

              }

            }];

        }else{

          //本地有缓存,直接返回图片

          NSData *imgData = UIImageJPEGRepresentation(image,1.0);

          result(imgData);

        }

      }

  }

}

(2)Android开发


Android端的代码实现逻辑与iOS一致,也是先判断Flutter调用的是本地还是网络图片,对于本地图片先根据文件名获取到图片的Bitmap,然后转成byte数组返回;对于网络图片的缓存和下载基于Glide组件实现,在获取到文件缓存或下载路径后,再将文件读取为byte数组返回。

六、插件测试

Flutter脚手架在创建插件的时候自动生成了example项目,该项目通过指定插件path的方式引用了我们正在开发中的组件,让我们在发布插件之前可以进行充分的测试。

native_image_view:

  path: ../

example项目除了开发调试之外,还是一种很好的插件使用示例。相比于文档,很多开发者更喜欢直接看插件example的代码实现。我们在main.dart中展示了网络图片的使用,本地图片需要原生项目中存在对应文件才可以。

main.dart:

String url = "https://iknow-pic.cdn.bcebos.com/79f0f736afc379313a380ef3e7c4b74542a91193";

//String url = "localImage://wdqy.jpeg";

@override

Widget build(BuildContext context) {

  return MaterialApp(

    home: Scaffold(

      appBar: AppBar(

        title: Text('example'),

      ),

      body: Center(

      child: NativeImageView(

        url: url,

        width: 300,

        height: 200,

      ),

    ),

  ));

}

七、插件发布

插件开发完成后就进入了发布环节,为了便于后续维护和用户反馈问题,我们将插件在github上进行维护,并在插件的pubspec.yaml文件中填写仓库地址。

name: native_image_view

description: 该组件提供了一种方式,可以让flutter通过methodChannel调用原生的本地和网络图片的加载

version: 0.0.1

repository: https://github.com/xxxx/native_image_view

在提交仓库之前,我们需要先运行dry-run命令检查组件目前是否符合发布要求。

flutter pub publish --dry-run

Flutter脚手架为我们创建的LICENSE文件是空的,需要开发者自行填写插件的开源协议。如果不填写的话dry-run不会提示,但在仓库发布那一步还是会报错。

1. 公共仓库

切记,发布在公共仓库中的插件将永久存在,flutter pub不允许开发者撤回已发布的插件,因为插件一旦发布就可能有项目依赖,而撤回组件将会破坏这种依赖关系。

直接使用publish命令,将插件发布到公共仓库:

flutter pub publish

在发布插件的过程中,可能会要求开发者登录谷歌账号进行验证,根据提示拷贝url地址在浏览器中打开,登录账户并授权即可。

在插件发布成功后,一般不能立即搜索到,需要等待pub仓库进行同步,大概15分钟左右,就可以在pub.dev/中搜索到刚刚发布的插件…

公共仓库插件的使用非常简单,在插件发布以后,任何一个项目都可以通过pubspec.yaml引用该插件。


dependencies:

  native_image_view: ^0.0.1
  

2. 私有仓库

Flutter默认发布到公共仓库,并且一旦发布就不能撤回。出于安全性考虑,在实际的业务开发中有一些组件我们暂时不想开源,而是仅限团队或公司内部使用。

这种业务场景中,一种选择是不发布组件,直接在pubspec.yaml中通过path指定本地路径、或者通过git指定仓库地址;另一种选择则是搭建内部pub仓库,将插件发布到私有仓库中。

(1)搭建私有仓库

Flutter官方提供了基于dart的pub_server组件,可以快速搭建本地运行的私有仓库服务器。pub_server没有提供类似公共仓库的web网站,但是在shelf_pubserver.dart文件中定义了仓库组件的上传、下载和删除等接口,如果有需要也可以基于该接口快速搭建web服务。

(2)配置dart运行环境


 brew tap dart-lang/dart

 brew install dart

(3)安装并运行pub_server

git clone github.com/dart-lang/p…

cd pub_server

pub get

dart example/example.dart -d /tmp/package-db -h 192.168.1.3 -p 8081

(4)发布到私有仓库

发布到私有仓库需要在pubspec.yaml文件中新增一个publish_to字段,指定私有仓库的地址。

publish_to: http://192.168.1.3:8081

在发布插件时需要在发布指令中指定私有仓库的地址。

flutter packages pub publish --server=http://192.168.1.3:8081

(5)引用私有仓库组件

不同于公共仓库,在引用私有仓库中的插件时需要在pubspec.yaml文件中,通过hosted参数指定私有仓库的地址。


native_image_view:

  version: 0.0.1

  hosted:

    name: native_image_view

    url: http://192.168.1.3:8081
    
转载自:https://juejin.cn/post/7376821583048343604
评论
请登录