Flutter | 如何優雅的開發一個外掛併發布到Dart倉庫?

Thatcher_Li發表於2021-05-13

什麼是 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 倉庫。這中間我們遇到了很多困難,踩過很多坑,尤其在最後的釋出步驟。但都一個個解決了。

如果你對我感興趣,請移步到 blogss.cn , 或關注公眾號:程式設計師小北,進一步瞭解。

  • 如果本文幫助到了你,歡迎點贊和關注 ❤️
  • 由於作者水平有限,文中如果有錯誤,歡迎在評論區指正 ✔️
  • 本文首發於掘金,未經許可禁止轉載 ©️

相關文章