Flutter | 如何优雅的开发一个插件并发布到Dart仓库?

什么是 Flutter 插件包?

Flutter 插件包与 Android Gradle 中的依赖包一样的意思。一些官方或者开源组织开发的包能显著提升我们的开发效率,减少开发成本。同时我们能从一些优秀的开源框架中学到很多知识,作为一个程序员,经常去 Github 逛一逛看看项目,或者 Fork 开源项目贡献代码,能得到很大的提升。

Flutter 包分为下面两类。

  1. Dart包:不依赖于特定平台,对 Flutter 框架具有依赖性,这种包仅用于Flutter。
  2. 插件包:依赖于特定平台,一种专用的 Dart 包,其中包含用 Dart 代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现,也就是说插件包括原生代码。

本文仅介绍 Flutter 插件包的整个开发与发布流程,至于 Dart 包,过程都是类似的,读者可以查阅相关文章进行了解。

创建 Flutter 插件包项目

正文开始前,读者需要对 Flutter 的平台通道有所了解,如果你还不知道,可以先阅读我之前写的 Flutter | 如何优雅的调用 Android 原生方法?,然后再回来继续本文的学习。如果你已经掌握平台通道的相关知识,跟着我的步伐,继续往下~

本文将带读者在 Android 平台上实现一个调节音量大小的插件包项目,并发布到 Dart 仓库。首先打开 Android Studio,创建 Flutter Plugin 项目,如下。

创建项目

整个项目创建完成后,目录结构是下面这个样子的,和 Flutter 项目差不多。系统会根据你创建项目所填的包名(我的包名是 cn.blogss.volume_control),自动在 android 和 lib 目录下生成两个类,分别是VolumeControlPlugin.ktvolume_control.dart

目录结构

VolumeControlPlugin.kt 实现了 FlutterPlugin 和 MethodCallHandler 接口。可以发现这和我们编写 Android 端平台通道代码基本一样。实例化了一个名叫 volume_control 的平台通道,之后我们只要在 onMethodCall 方法中根据业务逻辑处理来自平台的消息并返回结果即可。

/** VolumeControlPlugin */
class VolumeControlPlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel : MethodChannel

  // Flutter Engine 启动时会自动调用这个方法,实例化平台通道
  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "volume_control")
    channel.setMethodCallHandler(this)
  }

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {  // 这里处理来自平台的消息
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
      result.notImplemented()
    }
  }

// Flutter Engine关闭时,释放内存
  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}

volume_control.dart 和我们编写 Flutter 端平台通道代码基本一样。内部实例化一个平台通道,然后可以在内部编写各种异步方法,来与特定平台进行通信,接收平台返回的结果。

class VolumeControl {
  static const MethodChannel _channel =
      const MethodChannel('volume_control');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

以上就是创建 Flutter 插件项目的整个过程,向读者介绍了系统为我们自动生成的两个重要类。之后我们的开发重心围绕这两个类来开展

编写调节音量代码并测试

Android 端代码

首先在 VolumeControlPlugin.kt 编写需要实现的方法。如下,我写了四个对应的方法名供 Flutter 端来调用。分别是设置音量最大范围、获取当前电量、改变媒体音量、改变系统音量。VolumeManager内部实现了这四个方法的具体逻辑,由于篇幅关系,且本文的目的是带读者熟悉整个 Flutter 插件开发流程,这里不贴出 VolumeManager 类的源代码,也不讲解其实现细节。代码放在 volume_flutter,感兴趣的读者可以去看看。

/** VolumeControlPlugin */
class VolumeControlPlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel : MethodChannel
  private lateinit var volumeManager: VolumeManager

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "volume_control")
    channel.setMethodCallHandler(this)

    volumeManager = VolumeManager(flutterPluginBinding.applicationContext)
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    when(call.method){
      "setMaxVol" -> {    // 设置最大音量范围
        volumeManager.setMaxVol(call.arguments as Double);
      }
      "getCurrentVol" -> {    // 获取当前音量
        volumeManager.setAudioType(call.arguments as Int)
        result.success(volumeManager.currentVolume);
      }
      "changeMediaVoice" -> { // 改变媒体音量
        volumeManager.setAudioType(VolumeManager.TYPE_MUSIC)
        val curVoice = volumeManager.setVoice(call.arguments as Double);
        result.success(curVoice)
      }
      "changeSysVoice" -> {   //改变系统音量
        volumeManager.setAudioType(VolumeManager.TYPE_SYSTEM)
        val curVoice = volumeManager.setVoice(call.arguments as Double);
        result.success(curVoice)
      }
      else -> {
        result.notImplemented()
      }
    }
  }
}

Flutter 端代码

volume_control.dart 中编写 4 个异步方法,来调用上面 Android 端我们写好的处理方法,如下。

class VolumeControl {
  static const MethodChannel _channel = const MethodChannel('volume_control');

  /// 设置音量最大范围
  /// setMaxVol 方法考虑到了音量的最大值可以自由设置,如果不使用这个方法,默认音量最大值是 100
  static Future<void> setMaxVol(double num) async{
    await _channel.invokeMethod("setMaxVol",num);
  }

  /// 获取当前音量
  static Future<double> getCurrentVol(AudioType audioType) async{
    return await _channel.invokeMethod("getCurrentVol",_getStreamInt(audioType)) as double;
  }

  /// 改变媒体音量
  static Future<double> changeMediaVoice(double num) async{
    return await _channel.invokeMethod("changeMediaVoice",num) as double;
  }

  /// 改变系统音量
  static Future<double> changeSysVoice(double num) async{
    return await _channel.invokeMethod("changeSysVoice",num) as double;
  }
}

enum AudioType {
  /// Controls the Voice Call volume
  STREAM_VOICE_CALL,
  /// Controls the system volume
  STREAM_SYSTEM,
  /// Controls the ringer volume
  STREAM_RING,
  /// Controls the media volume
  STREAM_MUSIC,
  // Controls the alarm volume
  STREAM_ALARM,
  /// Controls the notification volume
  STREAM_NOTIFICATION
}

int _getStreamInt(AudioType audioType) {
  switch (audioType) {
    case AudioType.STREAM_VOICE_CALL:
      return 0;
    case AudioType.STREAM_SYSTEM:
      return 1;
    case AudioType.STREAM_RING:
      return 2;
    case AudioType.STREAM_MUSIC:
      return 3;
    case AudioType.STREAM_ALARM:
      return 4;
    case AudioType.STREAM_NOTIFICATION:
      return 5;
    default:
      return null;
  }
}

在 Flutter 页面看看效果

Android 端和 Flutter 端的代码我们编写完毕,现在在项目生成的 example 目录下的 main.dart来编写页面示例代码,来展示插件的功能。注意 example 是开发者写给使用者看的,告诉他们这个插件如何使用的一个 Flutter 项目,相当于帮助文档,我觉得这点很好,极大了加快了我们的上手速度。

main.dart

main.dart 中用一个 Slider 滑块组件来展示下效果。按以下步骤编码。

  1. 进入页面的时候调用 getCurrentVol 方法来获取当前媒体音量,显示初始状态。
  2. 滑动滑块调用 changeMediaVoice 方法来改变媒体音量。

main.dart 页面代码如下,也很简单。

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  double _musicVoice;

  @override
  void initState() {
    super.initState();
    ///1.获取当前媒体音量
    initCurrentVol();
  }

  /// 获取当前媒体音量
  Future<void> initCurrentVol () async{
    _musicVoice = await VolumeControl.getCurrentVol(AudioType.STREAM_MUSIC);
    if(!mounted) return;
    setState(() {});
  }

  /// 改变媒体音量
  Future<void> changeMediaVoice(double vol) async{
    await VolumeControl.changeMediaVoice(vol);
    _musicVoice = vol;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: (_musicVoice != null) ? Slider(
            value: _musicVoice,
            min: 0,
            max: 100,
            inactiveColor: Colors.grey,
            activeColor: Colors.blue,
            onChanged: (vol){
              /// 2. 滑动改变媒体音量
              changeMediaVoice(vol);
            },
          ): Container(),
        ),
      ),
    );
  }
}

实际效果如下,滑动滑块时,系统媒体音量也随之改变。我的测试机型是小米 MI 6X。其他机型可能会有差异,请读者注意。

音量调节.gif

将开发好的插件包上传到 Dart 仓库

我们的 Flutter 插件包整个开发流程就结束了。现在将它上传到 Dart 仓库,方便其他开发者可以使用这个插件包。在发布之前,检查 LICENSEpubspec.yamlREADME.md 以及 CHANGELOG.md 四个文件。

选择开源许可证(LICENSE)

软件开源许可证,大概有上百种。最流行的六种 --- GPLBSDMITMozillaApacheLGPL。读者可以从 Choose an open source license 选择适合自己的证书,我这里选择 MIT。

选择证书

将复制的内容粘贴到 LICENSE,用当前年份替换掉 [year],版权所有者替换掉 [fullname]。如下图,证书就算是弄完了。

MIT LICENSE

修改 pubspec.yaml

name: volume_control
description: A new Flutter plugin.
version: 0.0.1
author:
homepage:

这里按实际情况修改 description 插件的简要描述,version 插件的版本,homepage 项目主页,其中 author 已经不支持使用了,读者需要直接删除,不然后面检查会不通过,修改后如下。

name: volume_control
description: A Flutter plugin which can control android volume.
version: 0.0.1
homepage: https://github.com/liqvip/volume_control

修改 README.md 和 CHANGELOG.md

README.md 文件不用多说,读者可以根据自己插件是干什么的、有什么用、使用方法等自由发挥。
CHANGELOG.md 文件用来记录每个版本的更改。也是根据实际情况来填写。

## 0.0.1

initial commit

很简单,对于 0.0.1 版本我只填了一句话,嘻嘻~

开始上传

  1. 首先在 Android Studio Termial 中输入如下命令,来检查我们编写的好的上述文件是否符合发布的要求。
flutter pub publish --dry-run
  1. 如果检查没有问题,控制台会输出如下提示信息。
Package has 0 warnings.
  1. 然后输入如下命令,开始上传
flutter pub publish --server=https://pub.dartlang.org

会提示你一旦发布就是永久的,不能够取消发布。输入 y 继续下一步

Publishing is forever; packages cannot be unpublished.
Policy details are available at https://pub.dev/policy

Do you want to publish volume_control 0.0.1 (y/N)?

控制台接着会输出一个链接,这里我们要复制这个链接到浏览器打开,然后会提示你登录验证谷歌邮箱,没有的需要用 VPN 注册一个谷歌邮箱。

Do you want to publish volume_control 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%3A55779&code_challenge=t9GweRvzHgPt6F1-1I42-3e8eg1MeA7xovsNLCsDHks&code_challenge_method=S256&scope=openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email
Then click "Allow access".

Waiting for your authorization...

下面是我用自己的邮箱,验证通过了的结果。

验证通过

回到控制台,你可能会看到下面的报错信息。报错信息告诉我们,收到了验证信息正在处理但最终却走不到下一步,最后只能超时了。很明显这是网络问题。

Waiting for your authorization...
Authorization received, processing...
It looks like accounts.google.com is having some trouble.
Pub will wait for a while before trying to connect again.
OS Error: 信号灯超时时间已到
, errno = 121, address = accounts.google.com, port = 56479
pub finished with exit code 69

这是因为即使你设置了代理,此时终端中的 http 和 https 并不会被代理,所以我们需要设置一下终端代理。根据下面提供的命令,读者可以在不同的操作系统上设置终端代理,注意 http 和 https 都要设置,还有我的 ssr 代理端口是 1080,读者需要根据你的实际代理端口填写

Windows
# 设置代理
set http_proxy=http://127.0.0.1:1080
set https_proxy=http://127.0.0.1:1080
# 验证代理是否设置成功
curl -vv http://www.google.com
# 取消代理
set http_proxy=
set https_proxy=

Linux
export http_proxy=http://127.0.0.1:1080;
export https_proxy=http://127.0.0.1:1080;

根据上面提供的命令,在 Windows 下设置终端代理后,测试下代理是否设置成功,只需请求一下 Google。返回如下结果表示代理设置成功。

代理设置成功

代理设置完了之后,继续执行发布命令。

flutter pub publish --server=https://pub.dartlang.org

结果如下,显然这次网络问题已经解决了,但是 Dart 仓库上有一个和我们同名的插件包。所以我们将volume_control 改成 volume_flutter ,并将其他相关的类名也修改一下,然后继续发布。这里项目名最好先去 Dart 仓库搜一搜有没有被占用。如果被占用了就取个不同的名字。不然这里这很难受了,555~

插件包同名了

10分钟过去,我名称改完了,兄弟们,继续执行发布命令。上传成功了,激动得飞起,嘻嘻~

发布成功

在 Dart 仓库查看

最后一步去 Dart 仓库 volume_flutter 查看最后的战果。注意仓库会有延迟,没那么快就可以找到你刚刚上传的插件,需要等待个几分钟。结果如下,我们完成了整个插件的开发与发布过程。

volume_flutter

写在最后

本文带领读者实现了一个在 Flutter 中调节 Android 音量的插件项目,并将其发布到了 Dart 仓库。之后如果有开发者想使用这个插件,只需要在 pubspec.yaml 中添加如下依赖即可。使用方法和我们在 exmaple 目录编写的示例代码一致。

dependencies:
  volume_flutter: ^0.0.1

通过本文,读者应该能够完全掌握如何开发一个插件包并将其发布到 Dart 仓库。这中间我们遇到了很多困难,踩过很多坑,尤其在最后的发布步骤。但都一个个解决了。

如果你对我感兴趣,请移步到 http://blogss.cn
或关注公众号:程序员小北,进一步了解。

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

推荐阅读更多精彩内容