Flutter插件开发-(进阶篇)

news2024/11/14 16:57:04

一、概述

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机制提供了三种交互方式:

  • BasicMessageChannel :用于传递字符串和半结构化信息;

  • MethodChannel :用于传递方法调用和处理回调;

  • EventChannel:用于数据流的监听与发送。

这三种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 Package(包):仅包含dart代码,一般是对flutter特定功能的封装实现,例如用于网络请求的http包。

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

  • Flutter插件可以通过Android Studio创建(需要在Android Studio中先安装Dart和Flutter插件),或者使用命令行创建。

  • 创建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<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, '8.0'

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

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

NativeImageViewPlugin.m:

#import "NativeImageViewPlugin.h"
#import <SDWebImage/SDWebImage.h>

@implementation NativeImageViewPlugin
//组件注册接口,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);
  }
}

- (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:

//新的插件注册接口
@Override
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
  channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.tencent.game/native_image_view");
  channel.setMethodCallHandler(this);
  setContext(flutterPluginBinding.getApplicationContext());
}

@Override
public 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);
}

@Override
public 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<File>() {
      @Override
      public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> 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";
@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: 

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


flutter pub publish --dry-run

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431731.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023-04-15 学习记录--C/C++-mac vscode配置并运行C/C++

mac vscode配置并运行C/C 一、vscode安装 ⭐️ 去官网下载安装mac版的vscode。 二、vscode配置 ⭐️ &#xff08;一&#xff09;、安装C/C扩展插件及必装好用插件 1、点击左边的 图标(扩展: 商店)&#xff0c;如下图&#xff1a; 2、先安装 C/C、C/CExtension Pack插件&…

大话数据结构-C(2)

二&#xff1a;算法 解决特定问题求解步骤的描述&#xff0c;在计算机中表现为指令的有限序列&#xff0c;并且每条指令表示一个或多个操作。 2.1 算法的特性 算法具有五个基本特性&#xff1a;输入、输出、有穷性、确定性、可行性。 1&#xff09;输入输出&#xff1a; 算法具…

Python --- 文件操作

目录 前言 一、open()函数 1.只读模式 r 2.只写模式 w 3.追加模式 a 二、操作其他文件 1.Python 操作二进制 2.Python 操作 json 文件 三、关闭文件 四、上下文管理器 五、文件指针位置 前言 在实际操作中&#xff0c;通常需要将数据写入到本地文件或者从本地文件中…

南方猛将加盟西方手机完全是臆测,他不会希望落得兔死狗烹的结局

早前南方某科技企业因为命名的问题闹得沸沸扬扬&#xff0c;于是一些业界人士就猜测该猛将会加盟西方手机&#xff0c;对于这种猜测可以嗤之以鼻&#xff0c;从西方手机以往的作风就可以看出来它向来缺乏容纳猛将的气量。一、没有猛将的西方手机迅速沉沦曾几何时&#xff0c;西…

【项目】bxg基于SaaS的餐掌柜项目实战(2023)

基于SaaS的餐掌柜项目实战 餐掌柜是一款基于SaaS思想打造的餐饮系统&#xff0c;采用分布式系统架构进行多服务研发&#xff0c;共包含4个子系统&#xff0c;分别为平台运营端、管家端&#xff08;门店&#xff09;、收银端、小程序端&#xff0c;为餐饮商家打造一站式餐饮服务…

如何用ChatGPT翻译?ChatGPT提升翻译速度,亲测有效

作为翻译新手&#xff0c;你是否为翻译不准确不地道而烦恼&#xff1f; 随着ChatGPT的大火&#xff0c;很多聪明的翻译已经开始使用ChatGPT辅助自己提升翻译能力和速度了。 想用ChatGPT翻译&#xff0c;首先要知道在哪里可以使用ChatGPT&#xff01;在国内选择不用注册不用登录…

python实现批量生成带内容的文件夹

我工作的时候经常遇到这个问题&#xff1a;需要批量生成带内容的文件夹来辅助工作。 我有8种不同名字的文件夹 每个文件夹下面都有以日期命名的文件夹 日期文件夹里面会记录我当天需要记录的东西。 我需要实现的功能是&#xff1a; 1.输入一个天数N&#xff0c;生成N天以前…

机器学习 day05(多元线性回归,向量化)

单个特征&#xff08;变量&#xff09;的线性回归模型 房子的价格仅由房子的大小决定&#xff0c;如图&#xff1a; 多个特征&#xff08;变量&#xff09;的线性回归模型 房子的价格由房子的大小&#xff0c;房子有多少个卧室&#xff0c;房子有几层&#xff0c;房子住了多…

代码随想录|day44|动态规划part06● 完全背包● 518. 零钱兑换 II ● 377. 组合总和 Ⅳ

完全背包 理论基础 视频&#xff1a;带你学透完全背包问题&#xff01; 和 01背包有什么差别&#xff1f;遍历顺序上有什么讲究&#xff1f;_哔哩哔哩_bilibili 链接&#xff1a;代码随想录 //先遍历背包还是先遍历物品是没有影响的。可以和01背包保持一致&#xff0c;都先遍历…

vue-自定义指令

需求1&#xff1a;定义一个v-big指令&#xff0c;和v-text功能类似&#xff0c;但会把绑定的数值放大10倍。 需求2&#xff1a;定义一个v-fbind指令&#xff0c;和v-bind功能类似&#xff0c;但可以让其所绑定的input元素默认获取焦点。 语法&#xff1a; 局部使用&#xff…

【硬件外设使用】——I2C

【硬件外设使用】——I2CI2C基本概念I2C通信协议I2C使用方法pyb.i2cmachine.i2cI2C可用的传感器I2C基本概念 I2C是"Inter-Integrated Circuit"的缩写&#xff0c;也被称为TWI (Two Wire Interface)。 它是一种串行通信协议&#xff0c;用于连接多个设备或组件。 I2…

记一次idea+Dockerfile+docker部署

软件版本&#xff1a;idea:2021.3,docker:19.03.9,服务器&#xff1a;centos7.8 1.centos7服务器配置 在服务器上编辑docker文件 vi /usr/lib/systemd/system/docker.service修改以ExecStart开头的行 ExecStart/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/ru…

计算机视觉基础__图像特征

计算机视觉基础__图像特征 本篇目录&#xff1a; 一、前言 二、位图和矢量图概念 三、图像的颜色特征 四、RGB 颜色空间 五、HSV 颜色空间 六、HLS 颜色空间 七、实例代码 八、参考资料 一、前言 传统图像处理&#xff0c;需要找出图片中的关键特征&#xff0c;然后对这…

30天学会《Streamlit》(5)

30学会《Streamlit》是一项编码挑战&#xff0c;旨在帮助您开始构建Streamlit应用程序。特别是&#xff0c;您将能够&#xff1a; 为构建Streamlit应用程序设置编码环境 构建您的第一个Streamlit应用程序 了解用于Streamlit应用程序的所有很棒的输入/输出小部件 第6天 - 将…

GO变量的使用

Go变量的使用注意事项 &#xff08;1&#xff09;第一种&#xff1a;指定了变量类型&#xff0c;但是声明后若不赋值&#xff0c;则使用默认值 &#xff08;2&#xff09;第二种&#xff1a;根据值自行判断我们的变量类型**&#xff08;类型推导&#xff09;** var num10.00 …

Python ---> 衍生的数据技术

我的个人博客主页&#xff1a;如果’真能转义1️⃣说1️⃣的博客主页 关于Python基本语法学习---->可以参考我的这篇博客&#xff1a;《我在VScode学Python》 随着人工智能技术的发展&#xff0c;挖掘和分析商业运用大数据已经成为一种推动应用&#xff0c; 推动社会发展起着…

接口优化方案

前言 最近随着国产化热潮&#xff0c;公司的用于营业的电脑全部从windows更换成了某国产化电脑&#xff0c;换成国产化之后&#xff0c;我们系统的前台web界面也由之前的jsp页面重构成vue.所以之前的一体式架构也变成了前后端分离的架构。但是在更换过程后&#xff0c;发现一些…

蓝绿部署技术方案

文章目录 ngx_lua介绍Nginxluangx_lua模块的原理&#xff1a;ngx_lua 模块执行顺序与阶段ngx_lua应用场景 JWTnginx镜像构造lua-redis蓝绿部署特性注意&#xff1a;蓝绿部署架构图nginx配置服务脚本部署使用职责分工 ngx_lua介绍 Nginx Nginx是Web服务器、HTTP反向代理和TCP代…

apache+tomcat实现动静分离和负载均衡

文章目录 ApacheTomcat整合环境通过JK实现动静分离编译mod_jk.so创建测试页面配置jk模块启动apache和tomcat测试。 ApacheTomcat负载均衡配置测试页配置mod_jk文件配置worker.properties测试 ApacheTomcat整合 Tomcat作为一个Servlet容器&#xff0c;可以用于运行Java Web应用…

Unity之c#专题篇——【不动如山核心章】

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…