Flutter 中 Package 及 Plugin 开发

Flutter 三方的工具有两种,一种是包(Package),一种是插件(Plugin)。这两种差别在于 Plugin 不仅包含了 Dart 代码,还包含了 iOS 以及 Android 的原生代码,比如常用的 image_picker。而 Package 仅仅是包含 Dart 代码的库。

Package 开发

  • 通过命令创建 Package

要创建 Dart 包,使用参数 --template=package 来执行 flutter create

flutter create --template=package 'package_name"
  • 通过 Android Studio 创建 Package

通过 Android Studio 创建 Package 的时候 Project type 选择 Package 类型。

这里我们通过对微信 Demo 中的索引控件代码进行抽取,创建一个自己的 Package 并在项目中进行使用。

代码抽取

library chenxi_chat_index_bar;

import 'package:flutter/material.dart';

class IndexBar extends StatefulWidget {
  final void Function(String str)? indexBarCallBack;
  const IndexBar({this.indexBarCallBack, Key? key}) : super(key : key);

  @override
  _IndexBarState createState() => _IndexBarState();
}

//获取选中的 item 文字
int _getIndex(BuildContext context, Offset globalPosition) {
  //拿到点击小部件的盒子,也就是索引条
  RenderBox box = context.findRenderObject() as RenderBox;
  // 拿到 y 值, globalToLocal 当前位置距离索引条左上角 (0, 0) 位置的距离 (x, y)
  double y = box.globalToLocal(globalPosition).dy;
  //算出字符的高度
  var itemHeight = screenHight(context) / 2 / INDEX_WORDS.length;
  //算出第几个 item
  int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
  return index;
}

class _IndexBarState extends State<IndexBar> {

  Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
  Color _textColor = Colors.black;
  double _indicatorY = 0.0;
  String _indicatorText = 'A';
  bool _indicatorHidden = true;

  @override
  Widget build(BuildContext context) {
    final List<Widget> _words = [];
    for (int i = 0; i < INDEX_WORDS.length; i++) {
      _words.add(
          Expanded(child: Text(INDEX_WORDS[i],
            style: TextStyle(fontSize: 10, color: _textColor),))
      );
    }

    return Positioned(
      right: 0.0,
      top: screenHight(context) / 8,
      height: screenHight(context) / 2,
      width:120,
      child: Row(
        children: [
          Container(
            alignment: Alignment(0, _indicatorY),
            width: 100,
            child: _indicatorHidden ? null : Stack(
              alignment: Alignment(-0.2, 0),
              children: [
                Image(image: AssetImage('images/气泡.png'), width: 60,),
                Text(
                  _indicatorText,
                  style: TextStyle(
                    fontSize: 35, color: Colors.white,
                  ),
                )
              ],
            ),
          ), //指示器
          GestureDetector(
            onVerticalDragUpdate: (DragUpdateDetails details){
              final Function(String str) callBack = widget.indexBarCallBack as Function(String str);
              int index = _getIndex(context, details.globalPosition);
              callBack(INDEX_WORDS[index]);
              setState(() {
                _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                _indicatorText = INDEX_WORDS[index];
                _indicatorHidden = false;
              });
            },
            // 索引条点击
            onVerticalDragDown: (DragDownDetails details){
              final Function(String str) callBack = widget.indexBarCallBack as Function(String str);
              int index = _getIndex(context, details.globalPosition);
              callBack(INDEX_WORDS[index]);
              setState(() {
                _bkColor = Color.fromRGBO(1, 1, 1, 0.4);
                _textColor = Colors.white;

                _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                _indicatorText = INDEX_WORDS[index];
                _indicatorHidden = false;
              });
            },
            // 索引条点击取消
            onVerticalDragEnd: (DragEndDetails details){
              setState(() {
                _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
                _textColor = Colors.black;

                _indicatorHidden = true;
              });
            },
            child: Container(
              width: 20,
              color: _bkColor,
              child: Column(
                children: _words,
              ),
            ),
          ), //索引条
        ],
      ),
    );
  }
}

//主题色
const Color CahtThemColor = Color.fromRGBO(230, 230, 230, 1.0);

//屏幕宽高
double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double screenHight(BuildContext context) => MediaQuery.of(context).size.height;

const INDEX_WORDS = [
'🔍',
'☆',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];

首先我们把所有索引控件相关的代码都抽取到了 chenxi_chat_index_bar.dart 文件中。

Package 中资源文件处理

首先是不建议在 Package 中使用资源文件,因为会给别人的使用造成不方便。但是如果是 UI 界面相关的资源这个使用是我们避免不了的,但是也是可以处理。

首先我们创建一个 images 文件夹,并把图片资源放入进来。

pubspec.yaml 中进行相关配置。

发布相关配置

同样是在 pubspec.yaml 中进行发布相关的配置。

  • namePackage 名称
  • descriptionPackage 的描述
  • versionPackage 的版本号
  • homepage :可以放一个自己相关的网址链接,需要注意的是网址可以访问,不然会被扣分。

发布 Package

  1. 检查 Package
flutter packages pub publish --dry-run

cd 到要发布的 Package 文件目录下,如果没有报错就代表没有缺失的信息,可以进行下一步。

  1. 发布
flutter packages pub publish

注意:目前发布插件跟包都需要 Google 账号,同时需要翻墙。

Do you want to publish chenxi_chat_index_bar 0.0.1 (y/N)? y
Pub needs your authorization to upload packages on your behalf.
In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A56076&code_challenge=jjz5gCzNNt_s5GYyQ3L_Pz4u_mSErJgx9BXoxUlY2XY&code_challenge_method=S256&scope=openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email
Then click "Allow access".

有时就算是翻墙也并不能解决问题,因为我们还配置了相关镜像,Flutter 官方就建议通过镜像的配置,所以在发布插件或者包的时候,就会因为镜像出现以下错误。

那么解决问题也很简单,就是指定服务器发布。

  • 指定服务器发布
flutter packages pub publish --server=https://pub.dartlang.org
  • LICENSE 证书问题

紧接着大家应该会遇到这个错误,就是版权的问题,之前是不需要的,但是现在要求必须要有。可以按下面步骤解决。

github 上创建仓库,勾选 Choose a license,选择 BSD 3-Clause,点击 Create repository 创建。

创建完成之后可以看到仓库就有了 LICENSE 文件。

打开 LICENSE 文件,复制所有内容。

把复制的内容粘贴到 Package 中的 LICENSE 文件中,再次执行发布命令。

这里可以看到已经发布成功了。

pub 官网,我们也可以看到自己发布的 Package 了。

使用自己的 Package

执行 Pub get 可以看到我们上传的 Package 包已经拉取到了本地。

使用时候引入以下头文件:

import 'package:chenxi_chat_index_bar/chenxi_chat_index_bar.dart' as chenxi;

as chenxi 是为了防止类名冲突。

使用代码:

chenxi.IndexBar(indexBarCallBack: (String str) {
            if(_groupOffsetMap[str] != null) {
              print(str);
              _scrollController!.animateTo(_groupOffsetMap[str], duration: Duration(microseconds: 100), curve: Curves.elasticIn);
            }
          }),//悬浮的索引条

Package 优化

资源文件处理

因为 Package 包上传的是 lib 文件,所以在 Package 中需要用到例如图片资源的话,需要把图片资源添加到 lib 路径下。

AssetImage('images/bubble.png', package: 'chenxi_chat_index_bar')

使用图片的时候可以指定包名。这里修改完之后需要对 Package 进行更新,需要修改 pubspec.yamlCHANGELOG.md 文件中的版本号重新发布。

  assets:
     - images/
     - packages/chenxi_chat_index_bar/images/bubble.png

这样的话在项目中引入 Package 的时候,图片资源配置需要如上所示,会比较麻烦。对于这种情况,可以让外部使用的时候传图片进来。

class IndexBar extends StatefulWidget {
  final void Function(String str)? indexBarCallBack;
  final ImageProvider image;
  const IndexBar(this.image, {this.indexBarCallBack, Key? key}) : super(key : key);
  @override
  _IndexBarState createState() => _IndexBarState();
}

展示的时候就直接用外部传入的 image 就好。

Image(image: widget.image, width: 60,),

分数优化

上传 Package 之后,官网会对我们的包进行评分,如果评分太低的话别人使用的时候就会有顾虑。这里总分是 130 分,我们目前得到的是 90 分,然后可以根据官网的提示进行优化。

  • The package description is too short

说明描述太短,这个我们增加下描述就可以了。

  • No example found 没有示例工程

github 上一样,当我们使用一些比较成熟的第三方的时候,都会有示例工程供我们参考。这里我们也创建一个示例工程。

新建一个 flutter 工程,并引入 chenxi_chat_index_bar 指定本地 path,然后添加示例代码。完成之后我们要上传示例程序。

在我们要发布的包 chenxi_chat_index_bar 中新建一个 example 文件,并把实例代码文件放到 example 文件下。

当我们发布的包中代码比较多的话可以对代码进行拆分,创建多个子文件。并通过代码建立依赖关系。

  • 指定子文件属于哪个主文件
part of 'chenxi_chat_index_bar.dart';
  • 指定主文件包含哪些子文件
part 'index_bar.dart';

做完以上操作之后就可以再更新版本号进行重新上传。

Package 链接:chenxi_chat_index_bar

Plugin 开发

Plugin 命令创建

  • 创建插件
    要创建插件包请使用 --template=plugin参数执行 flutter create
flutter create --template=plugin 'plugin_name'
  • 指定组织名称

使用 --org 选项指定你的组织,并使用反向域名表示法。

Dartpackage 是不需要组织名称的,--org 只有在 --template=plugin 时才生效。

flutter create --org com.example --template=plugin 'plugin_name
  • 指定其他语言
    由于 Plugin 包含 iOSAndroid 代码, 而他们分别都支持两种语言, iOS 支持 Object-C(默认)SwiftAndroid 支持 Java(默认)Kot in, 所以我们可以使用 -i-aiOSAndroid 指定语言。
flutter create--template=plugin-i swift-ako tL in'plugin_name

Android Studio 创建 Plugin

创建工程的时候我们选择 plugin,这里需要注意的是开发语言要选择自己熟悉的。

创建完成之后可以看到相对于 Package 这里多了示例工程文件,而且包含 androidios 文件夹。我们示例工程要实现的功能是获取手机的电池电量,这里只实现 ios 的相关代码。

代码实现

  • dart 代码
static Future<String> get platformBatteryLevel async {
    final int batteryLevel = await _channel.invokeMethod('getBatteryLevel');
    return batteryLevel.toString();
  }
  • oc 代码
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"chenxi_battery_level"
            binaryMessenger:[registrar messenger]];
  ChenxiBatteryLevelPlugin* instance = [[ChenxiBatteryLevelPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  if ([@"getPlatformVersion" isEqualToString:call.method]) {
    result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
  } else if ([@"getPlatformBatteryLevel" isEqualToString:call.method]) {
      result(@([self getBatteryLevel]));
  } else {
    result(FlutterMethodNotImplemented);
  }
}

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

发布 Plugin

  • 检查 Plugin
flutter packages pub publish --dry-run
  • 指定服务器发布
flutter packages pub publish --server=https://pub.dartlang.org

Plugin 的发布跟 Package 一样,cdPlugin 文件目录下执行发布命令。

Plugin 链接:chenxi_battery_level

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,106评论 6 542
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,441评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,211评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,736评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,475评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,834评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,829评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,009评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,559评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,306评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,516评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,038评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,728评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,132评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,443评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,249评论 3 399
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,484评论 2 379

推荐阅读更多精彩内容