Flutter第十五弹 Flutter插件

目标:

1.Flutter插件是什么?有什么作用?

插件 (plugin) 是 package 的一种,全称是 plugin package,我们简称为 plugin,中文叫插件。

2.怎么创建Flutter插件?

一、什么是插件

在flutter中,一个插件叫做一个package,使用packages的目的就是为了达到模块化,可以创建出可被复用和共享的代码,这和大多数编程语言中的模块、包的概念相同。创建出来的package可以在pubspec.yaml中直接依赖。

1.1 package组成

Flutter插件组成

  • 一个pubspec.yaml文件一个元数据文件,声明了声明了package的名称、版本、作者等信息。
  • 一个lib文件夹:包含里package的公开代码,文件夹至少需要存在<pakcage-name>.dart这个文件。

注意:<pakcage-name>.dart这个文件必须存在,因为这是方便使用的人快速import这个package来使用它,可以把它理解成一种必须要遵守的规则。

1.2 package分类

package可以分为两种:纯dart代码的package和带有特定平台代码的package。

  • Dart packages:这是一个只有dart代码的package,里面包含了flutter的特定功能,所以它依赖于flutter的framework,也决定了它只能用在flutter上。
  • plugin packages:这是一个既包含了dart代码编写的api,又包含了平台(Android/IOS)特定实现的package,可以被Android和ios调用。
  • FFI 插件
    用 Dart 语言编写针对一个或多个特定平台的 API,使用 Dart FFI (AndroidiOSmacOS)。

> 上面应该很好理解,可以理解成java jar包和Android sdk的区别。而要开发的日志插件就是第二种。

二、插件开发

2.1 创建package

可以使用AS创建插件

然后点击next。

 

然后点击 Create按钮,开始创建插件项目。

如果是采用Flutter命令创建项目

// 想要创建初始的 Flutter package,请使用带有 --template=package 标志的 flutter create 命令:

flutter create --template=package hello

2.2 项目文件结构

项目文件结构如下:

LICENSE 文件
大概率会是空的一个许可证文件。

  • test/hello_test.dart 文件

Package 的 单元测试 文件。

  • hello.iml 文件

由 IntelliJ 生成的配置文件。

  • .gitignore 文件

告诉 Git 系统应该隐藏哪些文件或文件夹的一个隐藏文件。

  • .metadata 文件

IDE 用来记录某个 Flutter 项目属性的的隐藏文件。

  • pubspec.yaml 文件

pub 工具需要使用的,包含 package 依赖的 yaml 格式的文件。

  • README.md 文件

起步文档,用于描述 package。

  • lib/hello.dart 文件

package 的 Dart 实现代码。

  • .idea/modules.xml.idea/workspace.xml 文件

IntelliJ 的各自配置文件(包含在 .idea 隐藏文件夹下)。

  • CHANGELOG.md 文件

又一个大概率为空的文档,用于记录 package 的版本变更。插件的native端实现

  • android/

插件包API的Android实现

  • iOS

插件包API的ios实现.

  • example/:

   一个依赖于该插件的Flutter应用程序,来说明如何使用它

lib库定义插件的主要功能。

2.3 实现插件

对于纯 Dart 库的 package,只要在 lib/<package name>.dart 文件中添加功能实现,或在 lib 目录中的多个文件中添加功能实现。
如果要对 package 进行测试,在 test 目录下添加 单元测试。

2.3.1 创建MethodChannel

项目默认生成了插件MethodChannel

1. 创建MethodChannel

flutter_log_plugin_method_channel.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'flutter_log_plugin_platform_interface.dart';

/// An implementation of [FlutterLogPluginPlatform] that uses method channels.
class MethodChannelFlutterLogPlugin extends FlutterLogPluginPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('flutter_log_plugin');

  @override
  Future<String?> getPlatformVersion() async {
    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }
}
2.定义插件方法 
  • 创建了一个MethodChannel,名称为flutter_log_plugin
  • 提供了一个方法访问getPlatformVersion

我们看看其调用的方式,通过创建的methodChannel.invokeMethod来调用原生实现。

final version = await methodChannel.invokeMethod<String>('getPlatformVersion');

2.3.2 插件方法Native实现

在项目 android 目录下,增加对 MethodChannel 方法的实现。

默认的插件实现的功能:Dart通过插件,获取 native端系统版本信息。

在android/src.main  下,实现了Native方法

package com.example.flutter_log_plugin

import androidx.annotation.NonNull

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

/** FlutterLogPlugin */
class FlutterLogPlugin: FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private lateinit var channel : MethodChannel

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

  /**
   * 方法调用处理
   *
   * @author zhouronghua
   * @time 2024/6/27 下午3:12
   */
  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
      result.notImplemented()
    }
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}
MethodChannel提供Flutter与原生系统之间的通信。
1.绑定MethodChannel: Activity attach到Flutter引擎
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private lateinit var channel : MethodChannel

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

因为Flutter提供的引擎是“flutter_log_plugin”名称,因此通过 命名找到对应的MethodChannel。

这样Activity就可以绑定Flutter MethodChannel,可以建立通信通道。

设置MethodCallHandler,注册插件到Flutter引擎。

channel.setMethodCallHandler(this)
2. Flutter调用Native方法

建立通道以后,Flutter调用Native端方法,方法名为getPlatformVersion,没有参数。

  /**
   * 方法调用处理
   *
   * @author zhouronghua
   * @time 2024/6/27 下午3:12
   */
  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
      result.notImplemented()
    }
  }

Native端接收方法调用的入口是 onMethodCall

  • 首先匹配方法名
  • 根据call的参数进行处理
  • 返回方法调用结果,通过result保存结果值。
  • 如果对应名称的方法未实现,则设置 result.notImplented()

当前获取安卓系统版本,返回结果是

"Android ${android.os.Build.VERSION.RELEASE}"
3.Activity与Flutter引擎断开时注销插件
  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }

 2.3.3 实现日志打印插件

1.声明接口方法

在lib插件的接口文件flutter_log_plugin_platform_interface.dart

声明接口方法 logI

  /**
   * 声明接口方法
   *
   * @author zhouronghua
   * @time 2024/6/27 下午4:05
   */
  void logI(String tag, String message) {
    throw UnimplementedError('logI() has not been implemented.');
  }
2. 插件端定义日志打印方法实现 logI
  /**
   * 日志打印:I级别
   * 说明: 日志打印不需要接收结果,因此不需要异步回调
   *
   * @author zhouronghua
   * @time 2024/6/27 下午3:45
   */
  @override
  void logI(String tag, String message) {
    /// 调用原生方法logI, 参数集为 {tag, message}
    /// 参数集按照键值对传递
    methodChannel.invokeMethod('logI', {"tag": tag, "message": message});
  }

调用的Native段方法名为 “logI”,对应的参数为:

{"tag": tag, "message": message}

参数一般使用键值对进行传递,参数之间采用逗号分隔。

注意,此处一定要使用注解 @override,否则调用logI编译报错

3.Native端实现方法接收处理

在android/src.main下,FlutterLogPlugin增加日志方法调用的实现。


    /**
     * 方法调用处理
     *
     * @author zhouronghua
     * @time 2024/6/27 下午3:12
     */
    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        if (call.method == "getPlatformVersion") {
            // 获取系统版本信息
            result.success("Android ${android.os.Build.VERSION.RELEASE}")
        } else if (call.method == "logI") {
            // 日志打印处理 logI(参数要与插件的键保持一致)
            final String tag = call.argument("tag")
            final String message = call.argument("message")
            android.util.Log.i(tag, message)
        } else {
            result.notImplemented()
        }
    }
4.Flutter插件入口添加日志打印方法

FlutterLogPlugin中,增加日志打印入口调用

/**
 * Flutter插件入口
 * 门面模式
 *
 * @author zhouronghua
 * @time 2024/6/27 下午4:11
 */
class FlutterLogPlugin {
  Future<String?> getPlatformVersion() {
    return FlutterLogPluginPlatform.instance.getPlatformVersion();
  }

  /**
   * 日志打印调用
   *
   * @author zhouronghua
   * @time 2024/6/27 下午4:11
   */
  void logI(String tag, String message) {
    return FlutterLogPluginPlatform.instance.logI(tag, message);
  }

}

这个是典型的门面模式,外部调用的使用不需要关注Flutter插件内部实现细节。

5.测试日志打印方法

在example/lib下,main.dart中,测试日志打印方法调用。


  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    // 调用日志打印
    _flutterLogPlugin.logI("MyApp", "开始初始化平台");
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      platformVersion = await _flutterLogPlugin.getPlatformVersion() ??
          'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }
    // 调用日志打印
    _flutterLogPlugin.logI("MyApp", "平台信息是:$platformVersion");

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

问题一:编译报错logI未实现

还是报错

Restarted application in 1,896ms.
E/flutter ( 2023): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: MissingPluginException(No implementation found for method logI on channel flutter_log_plugin)
E/flutter ( 2023): #0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
E/flutter ( 2023): <asynchronous suspension>
E/flutter ( 2023): 
E/flutter ( 2023): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: MissingPluginException(No implementation found for method logI on channel flutter_log_plugin)
E/flutter ( 2023): #0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
E/flutter ( 2023): <asynchronous suspension>
E/flutter ( 2023): 

test模块,flutter_log_plugin_test.dart缺少对应的logI方法的实现,因此报错。添加

class MockFlutterLogPluginPlatform 
    with MockPlatformInterfaceMixin
    implements FlutterLogPluginPlatform {

  @override
  Future<String?> getPlatformVersion() => Future.value('42');

  @override
  void logI(String tag, String message) {
    debugPrint("tag=$tag message=$message");
  }
}
6.打包apk

Terminal中,编译安卓APK。

$ cd example/android

$ gradlew clean

$ flutter build apk

如果报错,修正报错信息,重新打包试试。

出现下面的信息则编译成功。

安装运行APK,看看是否打印日志信息

能够输出对应的日志信息。

三、插件原理

Plugin其实就是一个特殊的Package。Flutter Plugin提供Android或者iOS的底层封装,在Flutter层提供组件功能,使Flutter可以较
方便的调取Native的模块。很多平台相关性或者对于Flutter实现起来比较复杂的部分,都可以封装成Plugin。

3.1 Platform Channel

Platform Channel:

1. Flutter App (Client),通过MethodChannel类向Platform发送调用消息;
2. Android Platform (Host),通过MethodChannel类接收调用消息;
3. iOS Platform (Host),通过FlutterMethodChannel类接收调用消息。

  • > PS:消息编解码器,是JSON格式的二进制序列化,所以调用方法的参数类型必须是可JSON序列化的。
  • > PS:方法调用,也可以反向发送调用消息。

3.2 安卓平台

FlutterActivity,是Android的Plugin管理器,它记录了所有的Plugin,并将Plugin绑定到FlutterView。 

3.3 理解Platform Channel工作原理


Flutter定义了三种不同类型的Channel,它们分别是

  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • MethodChannel:用于传递方法调用(method invocation)。
  • EventChannel: 用于数据流(event streams)的通信。

三种Channel之间互相独立,各有用途,但它们在设计上却非常相近。每种Channel均有三个重要成员变量:

  • name:  String类型,代表Channel的名字,也是其唯一标识符。
  • messager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。
  • codec: MessageCodec类型或MethodCodec类型,代表消息的编解码器。
  • Channel name

​   一个Flutter应用中可能存在多个Channel,每个Channel在创建时必须指定一个独一无二的name,Channel之间使用name来区分彼此。当有消息从Flutter端发送到Platform端时,会根据其传递过来的channel name找到该Channel对应的Handler(消息处理器)。

  • 消息信使:BinaryMessenger
  • 平台通道数据类型支持和解码器
  • 标准平台通道使用标准消息编解码器,以支持简单的类似JSON值的高效二进制序列化,例如 booleans,numbers, Strings, byte buffers, List, Maps(请参阅StandardMessageCodec了解详细信息)。 当您发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。

下表显示了如何在宿主上接收Dart值,反之亦然:

 3.4 解码器

 

消息解码器主要将二进制格式的数据转换为Handler能够识别的数据,Flutter定义了两种Codec:

MessageCodec和MethodCodec。

四、插件打包和发布

4.1 插件检查

一旦完成了 package 的实现,你便可以将其提交到 pub.dev 上,以便其他开发者可以轻松地使用它。

发布你的 package 之前,确保检查了这几个文件:pubspec.yamlREADME.md 和 CHANGELOG.md,确保它们完整且正确,另外,为了提高 package 的可用性,可以考虑加入如下的内容:

  • 代码的示例用法

  • 屏幕截图,GIF 动画或者视频

  • 代码库的正确指向链接

运行 dry-run 命令以检验是否所有内容都通过了分析:

$ flutter packages pub publish --dry-run

修正提示错误信息。 

pubspec.yaml 中anthor字段不需要了,直接删除

修改后再次执行。 

Package validation found the following potential issue:
* Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version. If this package needs Dart version 2.17.0-239.0.dev, consider publishing the package as a pre-release instead.
  See https://dart.dev/tools/pub/publishing#publishing-prereleases For more information on pre-releases.

Package has 1 warning.
pub finished with exit code 65

 

4.2 插件发布

最后一步是发布,请注意:发布是永久性 的,运行以下提交命令:

flutter pub publish

设置了中国镜像的开发者们请注意:目前所存在的镜像都不能(也不应该)进行 package 的上传。如果你设置了镜像,执行上述发布代码可能会造成发布失败。网络设定好后,无需取消中文镜像,执行下述代码可直接上传:

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

 

Dart 概览 | Dart

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值