文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Flutter插件开发-(进阶篇)

2023-09-03 12:14

关注

一、概述

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

除了使用仓库中的流行组件以外,在Flutter项目开发过程中面对通用业务逻辑拆分、或者需要对原生能力封装等场景时,开发者仍然需要开发新的组件。本文以一个具体的native_image_view插件为例,将从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中的Platform Channel机制提供了三种交互方式:

这三种channel虽然用途不同,但都包含了三个重要的成员变量:

(1)String name

表示channel的名字,在一个项目中可能会有很多的channel,每个channel都应该使用唯一的命名标识,否则可能会被覆盖。推荐的命名方式是组织名称加插件的名称,例如:com.tencent.game/native_image_view,如果一个插件中包含了多个channel可再根据功能模块进一步进行区分。

(2)BinaryMessager messager

作为Native与Flutter通信的载体,能够将codec转换后的二进制数据在Native与Flutter之间进行传递。每个channel在初始化时都要生成或提供对应的messager,如果channel注册了对应的handler,则messager会维护一个name与handler的映射关系。

Native平台在收到对方发来的消息后,meesager会将消息内容分发给对应的handler进行处理,在处理完成后还可以通过回调方法result将处理结果返回给Flutter。

 

(3)MessageCodec/MethodCodec codec

用于Native与Flutter通信过程中的编解码,在发送方能够将Flutter(或Native)的基础类型编码为二进制进行数据传输,在接收方Native(或Flutter)将二进制转换为handler能够识别的基础类型。

注:本文实现的native_image_share插件仅用到了最为常用的MethodChannel通信,Flutter通过MethodChannel将远程图片地址或本地图片文件名传递给原生侧,iOS和Android平台获取到图片后转换为二进制并通过result返回。更多关于MessageChannel和EventChannel的示例可以文末提供参考扩展阅读。

三、插件创建 

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

flutter create --org com.qidian.image --template=plugin --platforms=android,ios -i objc -a java native_image_view 

  • 使用--template=plugin声明创建的是同时包含了iOS和Android代码的plugin;

  • 使用--org选项指定组织,一般采用反向域名表示法;

  • 使用-i选项指定iOS平台开发语言,objc或者swift;

  • 使用-a选项指定Android平台开发语言,java或者kotlin。

lib目录用于存放package的代码实现,Flutter脚手架会自动生成一个与package同名的dart文件。
pubspec.yaml文件想必做过Flutter开发的同学都非常熟悉,我们开发package所依赖的package或者plugin都需要在该文件中声明。

四、插件开发 

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

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


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

 注:本文开发的插件仅用于介绍插件的开发和发布流程,不建议在生成环境中直接使用,关于图片二次缓存问题还可以参考扩展阅读中关于Texture(外接纹理)的文章。

1. Flutter端开发

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

native_image_view.dart:

class _NativeImageViewState extends State {  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, '8.0'

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

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

NativeImageViewPlugin.m:

#import "NativeImageViewPlugin.h"#import @implementation NativeImageViewPlugin//组件注册接口,Flutter自动调用+ (void)registerWithRegistrar:(NSObject*)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);  }}- (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);        }      }  }}@end

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

(2)Android开发

插件的Android平台使用Glide组件进行网络图片的下载和缓存,需要在build.gradle文件中声明依赖。

dependencies { implementation 'com.github.bumptech.glide:glide:4.11.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'}

为了兼容历史版本,Android端的插件需要在onAttachedToEngine和registerWith方法中实现相同的MethodChannel注册与监听的逻辑,onMethodCall用于处理Flutter中的方法调用,也提供了与iOS平台类似的MethodCall和Result对象。

android/src/main/xxxx/NativeImageViewPlugin.java:

//新的插件注册接口@Overridepublic void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {  channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.tencent.game/native_image_view");  channel.setMethodCallHandler(this);  setContext(flutterPluginBinding.getApplicationContext());}@Overridepublic void onDetachedFromEngine(FlutterPluginBinding binding) {  channel.setMethodCallHandler(null);}// Flutter-1.12之前的插件注册接口,功能与onAttachedToEngine一样public static void registerWith(Registrar registrar) {  NativeImageViewPlugin plugin = new NativeImageViewPlugin();  plugin.setContext(registrar.context());  final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.tencent.game/native_image_view");  channel.setMethodCallHandler(plugin);}@Overridepublic void onMethodCall(final MethodCall call,final Result result) {  if (call.method.equals("getImage")) {    getImageHandler(call,result);  } else {    result.notImplemented();  }}

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

public void getImageHandler(final MethodCall call,final Result result){  HashMap map = (HashMap) call.arguments;  String urlStr = map.get("url").toString();  Uri uri = Uri.parse(urlStr);  if("localImage".equals(uri.getScheme())){    String imageName = uri.getHost();    int lastIndex = imageName.lastIndexOf(".");    if(lastIndex > 0){      imageName = imageName.substring(0,lastIndex);    }    String imageUri = "@drawable/"+imageName;    int imageResource = context.getResources().getIdentifier(imageUri, null, context.getPackageName());    if(imageResource > 0){      Bitmap bmp = BitmapFactory.decodeResource(context.getResources(),imageResource);      ByteArrayOutputStream stream = new ByteArrayOutputStream();      bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);      byte[] byteArray = stream.toByteArray();      result.success(byteArray);    }else{      result.error("NOT_FOUND","file not found",call.arguments);    }  }else {    Glide.with(context).download(urlStr).into(new CustomTarget() {      @Override      public void onResourceReady(@NonNull File resource, @Nullable Transition transition) {        byte[] bytesArray = new byte[(int) resource.length()];        try {          FileInputStream fis = new FileInputStream(resource);          fis.read(bytesArray);          fis.close();          result.success(bytesArray);        } catch (IOException e) {          e.printStackTrace();          result.error("READ_FAIL",e.toString(),call.arguments);        }      }      @Override      public void onLoadFailed(@Nullable Drawable errorDrawable) {        super.onLoadFailed(errorDrawable);        result.error("LOAD_FAIL","image download fail",call.arguments);      }      @Override      public void onLoadCleared(@Nullable Drawable placeholder) {        result.error("LOAD_CLEARED","image load clear",call.arguments);      }    });  }}

五、插件测试

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

native_image_view:

path: ../

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

main.dart:

String url = "";//String url = "localImage://xxx.jpeg";@overrideWidget 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: 

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

flutter pub publish --dry-run

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

上一篇有教如何建立LICENSE文件,不再赘述。

来源地址:https://blog.csdn.net/RreamigOfGirls/article/details/130224297

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯