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 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 setMaxVol(double num) async{
    await _channel.invokeMethod("setMaxVol",num);
  }

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

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

  /// 改变系统音量
  static Future 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 {
  double _musicVoice;

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

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

  /// 改变媒体音量
  Future 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)

软件开源许可证,大概有上百种。最流行的六种 --- GPL、BSD、MIT、Mozilla、Apache 和 LGPL。读者可以从 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 ,
或关注公众号:程序员小北,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
  • 本文首发于掘金,未经许可禁止转载 ©️

你可能感兴趣的:(Flutter | 如何优雅的开发一个插件并发布到Dart仓库?)