Flutter开发之Package与Plugin

前言

flutter中有包和插件两个概念,插件 (plugin) 是 package 的一种,全称是 plugin package,我们简称为 plugin,中文叫插件。包(Package)主要指对flutter相关功能的封装,类似于Android中的插件和iOS中的三方库。而插件(Plugin)主要指通过插件调用原生的功能,如获取手机基本信息、获取原生的相机等。两者还是存在一定的差别的,Package一般只包含Dart代码,而插件除了包含有Dart外,还会包含有原生的语言,比如安卓中的JavaKotlin,和iOS中的Objective-CSwiftPackagePlugin都是为了封装一些基础组件、业务组件等,实现组件化开发,这样项目中多处用到可以快速引入实现功能。

  • Packages
    Dart package最低要求是包含一个pubspec.yaml文件,通常还包含一个lib目录。pubspec.yaml文件用于定义 package 名称、版本号、作者等其他信息的元数据文件;lib目录包含共享代码,其中至少包含一个<package-name>.dart文件。一个package可以包含依赖关系 (在pubspec.yaml文件里声明)、 Dart 库、应用、资源、字体、测试、图片和例子等。 pub.dev 上列出了很多 package,由 Google 工程师和 Flutter 和 Dart 社区的开发者开发和发布,你可以用在自己的应用里。
  • Plugins
    插件 (plugin package) 是一种特别的 package,特别指那些帮助你获得原生平台特性的 package。插件可以为 Android (使用 Kotlin 或 Java 语言)、 iOS (使用 Swift 或 Objective-C 语言)、Web、macOS、Windows、Linux 平台,或其任意组合的平台编写。比如:某个插件可以为 Flutter 应用提供使用原生平台的摄像头的功能。

下面一起来看下如何开发 Package 与 Plugin

一、 包(Package)

1.1 创建 Package

使用命令行创建:

flutter create --template=package hello

这将在 hello 目录下创建一个 package 项目,并且包含以下内容:

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 的版本变更。

推荐使用 Android Studio 进行创建,打开 Android Studio选择 New Flutter Project,首先需要选择该 package 使用的 flutter sdk

然后需要输入插件名、文件目录、插件类型(可以选择是 Package 还是 Plugin),支持开发平台及对应的开发语言:

这里输入项目名字为 flutter_package_demo,然后 Project type 选择 Package,平台暂时只选择 AndroidiOS,开发语言选择对应的 kotlinSwift 然后创建项目后,目录结构如下:

Package 项目的目录结构较为简单,主要是 lib 目录下的和项目同名的类flutter_package_demo.dart,该类主要是用于在里面暴露出包中的类,以便其它项目调用。Package 中的代码实现放在lib目录下即可,测试代码写在test目录下的flutter_package_demo_test.dart 中即可。

因为 Package 是个单纯的 Dart 库,没有 Android 和 iOS 壳工程,所以不能直接运行该Package 进行联调测试,可以在自己的项目中通过本地依赖path引入该 Package,然后可以通过在项目中调用进行测试联调。

1.2 定义 Package

这里创建好项目后,模版代码在 flutter_package_demo.dart 中已经生成了一个方法:

library flutter_package_demo;

/// A Calculator.
class Calculator {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

这里的 library flutter_package_demo 表明该 package 是一个可依赖包,包的名字为flutter_package_demo

1.3 验证 Package

由于这里不涉及页面 UI,该逻辑可以直接在单元测试中进行验证,代码写在test目录下的flutter_package_demo_test.dart中:

import 'package:flutter_test/flutter_test.dart';

import 'package:flutter_package_demo/flutter_package_demo.dart';

void main() {
  test('adds one to input values', () {
    final calculator = Calculator();
    expect(calculator.addOne(2), 3);
    expect(calculator.addOne(-7), -6);
    expect(calculator.addOne(0), 1);
  });
}

然后启动单元测试:

如果是涉及 UI 的 package,本地测试时,可以直接在当前项目下创建 example 目录,在里面开发功能验证代码,或者在外部使用的项目中通过如下方式在 pubspec.yaml 中引入包:

flutter_package_demo:
  path: ../ # package 所在的路径,绝对或者相对

1.4 包含 UI 的 Package

下面改造一下项目,让 package 提供一个对外可显示的 material dialog,先来定义package:

library flutter_package_demo;

export 'caculator.dart';
export 'dialogs.dart';

包名不变,把逻辑代码移除,导出了两个文件,第一个为上述的加1方法的抽离,第二个为我们要显示的 dialog

import 'package:flutter/material.dart';

import 'dialog_widget.dart';

class Dialogs {
  static const TextStyle titleStyle =
      TextStyle(fontWeight: FontWeight.bold, fontSize: 16);

  static const Color bcgColor = Color(0xfffefefe);

  static const Widget holder = SizedBox(
    height: 0,
  );

  static const ShapeBorder dialogShape = RoundedRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(16)));

  static const ShapeBorder bottomSheetShape = RoundedRectangleBorder(
      borderRadius: BorderRadius.only(
          topLeft: Radius.circular(16), topRight: Radius.circular(16)));

  /// 屏幕中弹出对话框
  static Future<void> materialDialog({
    required BuildContext context,
    Function(dynamic value)? onClose,
    String? title,
    String? msg,
    List<Widget>? actions,
    Widget customView = holder,
    bool barrierDismissible = true,
    Color? barrierColor = Colors.black54,
    String? barrierLabel,
    bool useSafeArea = true,
    bool useRootNavigator = true,
    RouteSettings? routeSettings,
    ShapeBorder dialogShape = dialogShape,
    TextStyle titleStyle = titleStyle,
    TextStyle? msgStyle,
    TextAlign? titleAlign,
    TextAlign? msgAlign,
    Color color = bcgColor,
    double? dialogWidth,
  }) async {
    await showDialog(
      context: context,
      barrierDismissible: barrierDismissible,
      barrierColor: barrierColor,
      barrierLabel: barrierLabel,
      useSafeArea: useSafeArea,
      useRootNavigator: useRootNavigator,
      routeSettings: routeSettings,
      builder: (context) {
        return Dialog(
          backgroundColor: color,
          shape: dialogShape,
          child: DialogWidget(
            title: title,
            dialogWidth: dialogWidth,
            msg: msg,
            actions: actions,
            customView: customView,
            titleStyle: titleStyle,
            msgStyle: msgStyle,
            titleAlign: titleAlign,
            msgAlign: msgAlign,
            color: color,
          ),
        );
      },
    ).then((value) => onClose?.call(value));
  }

  /// 底部弹出对话框
  static void bottomMaterialDialog({
    required BuildContext context,
    Function(dynamic value)? onClose,
    String? title,
    String? msg,
    List<Widget>? actions,
    Widget customView = holder,
    bool barrierDismissible = true,
    ShapeBorder dialogShape = bottomSheetShape,
    TextStyle titleStyle = titleStyle,
    TextStyle? msgStyle,
    Color color = bcgColor,
    bool isScrollControlled = false,
    bool useRootNavigator = false,
    bool isDismissible = true,
    bool enableDrag = true,
    RouteSettings? routeSettings,
    AnimationController? transitionAnimationController,
  }) {
    showModalBottomSheet(
      context: context,
      shape: dialogShape,
      backgroundColor: color,
      isScrollControlled: isScrollControlled,
      useRootNavigator: useRootNavigator,
      isDismissible: isDismissible,
      enableDrag: enableDrag,
      routeSettings: routeSettings,
      transitionAnimationController: transitionAnimationController,
      builder: (context) => DialogWidget(
        title: title,
        msg: msg,
        actions: actions,
        customView: customView,
        titleStyle: titleStyle,
        msgStyle: msgStyle,
        color: color,
      ),
    ).then((value) => onClose?.call(value));
  }
}

接着增加 example 工程来验证我们的 dialog,这里为了简单,以 Android 为例,就不添加iOS demo 工程了,项目结构如下:

在 example 工程的lib目录下创建 main.dart 编写功能验证代码:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_package_demo/caculator.dart';
import 'package:flutter_package_demo/dialogs.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'package 测试',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: SafeArea(
          child: Scaffold(
              backgroundColor: Colors.white,
              appBar: AppBar(
                title: const Text("package 测试"),
              ),
              body: const HomePage()),
        ));
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int value = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(value.toString(), textScaleFactor: 1.5),
          ElevatedButton(
              onPressed: () {
                setState(() {
                  value = Calculator().addOne(value);
                });
              },
              child: const Text('+1')),
          showPackageDialog(context),
        ],
      ),
    );
  }

  Widget showPackageDialog(BuildContext context) {
    return MaterialButton(
      color: Colors.grey[300],
      minWidth: 300,
      onPressed: () => Dialogs.materialDialog(
          msg: '确认关闭?',
          title: "关闭",
          color: Colors.white,
          context: context,
          dialogWidth: kIsWeb ? 0.3 : null,
          onClose: (value) => debugPrint("返回值: '$value'"),
          actions: [
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop(['取消了', 'List']);
              },
              child: const Text('取消', style: TextStyle(color: Colors.black)),
            ),
            OutlinedButton(
              onPressed: () {
                Navigator.of(context).pop(['关闭了', 'List']);
              },
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.red)),
              child: const Text('关闭', style: TextStyle(color: Colors.white)),
            ),
          ]),
      child: const Text("Show Material Dialog"),
    );
  }
}

页面效果如下:

二、 插件(Plugin)

Plugin 和 Package 最大的差别在于 Plugin 可以跨平台访问原生API提供的能力,然后通过插件调用原生 Android 和 iOS 的功能,以实现某些功能或者更好的用户体验,如获取手机版本信息、调用原生摄像头等,代码包含有 DartAndroid 原生代码、iOS原生代码。

2.1 创建Plugin

和创建Package类似,同样可以使用命令行进行创建:

flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello

flutter create --org com.example --template=plugin --platforms=android,ios -a java hello

flutter create --org com.example --template=plugin --platforms=android,ios -i objc hello

flutter create --org com.example --template=plugin --platforms=android,ios -i swift hello

4 条指令,可以根据选择的插件平台和开发语言,自由选择其中之一,这将在 hello 目录下创建一个插件项目,其中包含以下内容:

lib/hello.dart 文件
Dart 插件 API 实现。

android/src/main/java/com/example/hello/HelloPlugin.kt 文件
Android 平台原生插件 API 实现(使用 Kotlin 编程语言)。

ios/Classes/HelloPlugin.m 文件
iOS 平台原生插件 API 实现(使用 Objective-C 编程语言)。

example/ 文件
一个依赖于该插件并说明了如何使用它的 Flutter 应用。

默认情况下,插件项目中 iOS 代码使用 Swift 编写, Android 代码使用 Kotlin 编写

要在现有的插件项目中添加对特定平台的支持,请在项目目录运行 flutter create 命令,并加入--template=plugin。例如,要对现有的插件项目添加 Web 支持,请运行以下命令:

flutter create --template=plugin --platforms=web .

建议使用IDE进行创建,比较直观且易操作。

1、首先打开 Android Studio,然后点击 File->New->New Flutter Project

2、进入到选择 flutter sdk 页面,选择自己本地的 flutter,然后点击 next

3、选择 Plugin type,如下图所示。

前几个步骤和创建 Package 是一样的,唯一的区别在与第三步,这里 Project type选择Plugin

点击 create 后创建项目,项目结构如下:

lib 目录下的文件为插件的 flutter 具体实现,example 是插件测试项目,可以对 plugin进行功能测试,iOS 目录下为插件的iOS端原生实现,android 目录下为插件的 Android 端原生实现,test 目录为插件对应的单元测试。

2.2 定义Plugin

这里先重点关注 lib 目录下的三个文件:

  • 接口定义

flutter_plugin_demo_platform_interface.dart:定义接口的地方,该文件只定义方法。getPlatformVersion()为创建项目时,自动生成的方法,这里我们新增一个getScreenWidth()方法,来获取屏幕宽度:

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'flutter_plugin_demo_method_channel.dart';

abstract class FlutterPluginDemoPlatform extends PlatformInterface {
  /// Constructs a FlutterPluginDemoPlatform.
  FlutterPluginDemoPlatform() : super(token: _token);

  static final Object _token = Object();

  static FlutterPluginDemoPlatform _instance = MethodChannelFlutterPluginDemo();

  /// The default instance of [FlutterPluginDemoPlatform] to use.
  ///
  /// Defaults to [MethodChannelFlutterPluginDemo].
  static FlutterPluginDemoPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [FlutterPluginDemoPlatform] when
  /// they register themselves.
  static set instance(FlutterPluginDemoPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future<String?> getScreenWidth() {
    throw UnimplementedError('getScreenWidth() has not been implemented.');
  }
}
  • 接口实现

接口实现主要在 flutter_plugin_demo_method_channel 文件中,该文件为上述 interface 的子类,负责具体接口的实现,但方法的具体实现依赖于调用原生端的实现。

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

import 'flutter_plugin_demo_platform_interface.dart';

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

  @override
  Future<String?> getPlatformVersion() async {
    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
    return version;
  }

  @override
  Future<String?> getScreenWidth() async {
    final screenWidth = await methodChannel.invokeMethod<String>('getScreenWidth');
    return screenWidth;
  }
}

这里 getScreenWidth 的实现需要通过 methodChannel 调用原生方法。

  • 调用

和 plugin 项目同名的类 flutter_plugin_demo.dart 主要是供外部调用,该类中提供了所有对外调用的方法。这个类提供的方法包含两类,一类为使用 Dart 直接实现,一类为需要依赖于原生端实现,需要调用 flutter_plugin_demo_platform_interface 接口类中的方法

import 'flutter_plugin_demo_platform_interface.dart';

class FlutterPluginDemo {
  Future<String?> getPlatformVersion() {
    return FlutterPluginDemoPlatform.instance.getPlatformVersion();
  }

  Future<String?> getScreenWidth() {
    return FlutterPluginDemoPlatform.instance.getScreenWidth();
  }

  //  直接 dart 实现
  int getScreenHeight() {
    return 1080;
  }
}

2.3 iOS 原生实现

iOS 目录主要为 iOS 原生端实现,这里可以使用 example 下的 iOS 项目进行调试,因项目默认没有 pod 文件,所以需要对 example 目录下的 iOS 进行 pod install

这里需要注意的是,创建项目时选择的是 Swift 语言,所以具体实现是在 swift 类中,若选择的是oc,则实现是在 oc 类中。其实 swift 的实现也是通过oc的类调用的 swift

在 FlutterPluginDemoPlugin.m 类中可以看到调用的是SwiftFlutterPluginDemoPlugin.swift,具体的实现是在该类中:

#import "FlutterPluginDemoPlugin.h"
#if __has_include(<flutter_plugin_demo/flutter_plugin_demo-Swift.h>)
#import <flutter_plugin_demo/flutter_plugin_demo-Swift.h>
#else
// Support project import fallback if the generated compatibility header
// is not copied when this plugin is created as a library.
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
#import "flutter_plugin_demo-Swift.h"
#endif

@implementation FlutterPluginDemoPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  [SwiftFlutterPluginDemoPlugin registerWithRegistrar:registrar];
}
@end

SwiftFlutterPluginDemoPlugin 类中实现:

import Flutter
import UIKit

public class SwiftFlutterPluginDemoPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "flutter_plugin_demo", binaryMessenger: registrar.messenger())
    let instance = SwiftFlutterPluginDemoPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
      switch call.method {
        case "getScreenWidth":
            let screenRect = UIScreen.main.bounds
            let screenWidth = screenRect.size.width
            let screenHeight = screenRect.size.height
            result("iOS \(screenWidth)")
        case "getPlatformVersion":
            result("iOS " + UIDevice.current.systemVersion)
        default:
            result("no such method !")
      }
  }
}

2.4 Android 原生实现

因为创建项目时选择的语言为 kotlin,因此 Android 的原生实现在 kotlin 目录下与项目同名的 FlutterPluginDemoPlugin.kt 文件中:

package com.example.flutter_plugin_demo

import android.content.Context
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

/** FlutterPluginDemoPlugin */
class FlutterPluginDemoPlugin: 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
  private var context: Context? = null

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

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    when (call.method) {
      "getPlatformVersion" -> result.success("Android ${android.os.Build.VERSION.RELEASE}")
      "getScreenWidth" -> result.success("Android ${context?.resources?.displayMetrics?.widthPixels}")
      else -> result.notImplemented()
    }
  }

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

2.5 功能验证

完成以上步骤,我们的插件就开发完成了,接下来让我们在 example 项目中验证下我们刚刚开发的插件,在 example/lib/main.dart 中编写测试代码:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_plugin_demo/flutter_plugin_demo.dart';

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';
  String _screenWidth = 'Unknown';
  final _flutterPluginDemoPlugin = FlutterPluginDemo();

  @override
  void initState() {
    super.initState();
    initPlatformState();
    _getScreenWidth();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      platformVersion = await _flutterPluginDemoPlugin.getPlatformVersion() ??
          'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    // 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;
    });
  }

  Future<void> _getScreenWidth() async {
    String screenWidth;
    try {
      screenWidth = await _flutterPluginDemoPlugin.getScreenWidth() ?? '0';
    } on PlatformException {
      screenWidth = 'Failed to get screen width.';
    }
    if (!mounted) return;
    setState(() {
      _screenWidth = screenWidth;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Column(
          children: [
            Center(
              child: Text('Running on: $_platformVersion\n',textScaleFactor: 1.5),
            ),
            Center(
              child: Text('screen width: $_screenWidth\n',textScaleFactor: 1.5),
            ),
          ],
        ),
      ),
    );
  }
}

最终页面效果:

Android 截图iOS 截图

三、发布

发布主要有两种,一种是发布到 Flutter 的官方,一种是发布到自己公司的私有库中,两者发布步骤大部分都是相同的,在一些地方存在差异,下面对这两种发布方法进行介绍。

3.1 发布前准备

首先需要检查下 Package 或 Plugin 中是否包含有 pubspec.yamlREADME.mdCHANGELOG.mdLICENSE 这四个文件,需要检查下完整性。这四项内容默认创建的时候都会有,若缺少可以在项目终端中运行 flutter create .进行补全(注意有个点)。

  • pubspec.yaml

该文件主要是引用外部插件,其中 namedescriptionversionhomepage 需要填写完整,publish_to 指将该库发布到指定位置,发布到私有库需要,若发布到 pub.dev 则不需要。

  • README.md

该文件主要是写插件或包的内容,便于别人查看时告知该库的作用。

  • CHANGELOG.md

该文件主要是记录插件或包的版本修改记录,说明每个版本的更新内容。

  • LICENSE

该文件主要是添加许可证书,可根据需要填写

上述准备工作完成后,需要对项目进行校验,打开终端,进入插件或包的路径,然后运行

flutter packages pub publish --dry-run

然后查看运行结果,如果警告信息为0,说明没什么问题,就可以进行发布了。

3.2 发布

校验完成以后就可以发布了,还是使用命令行,最好先科学上网,然后再运行命令进行发布:

flutter packages pub publish 

发布到官方和私有库有点区别,若发布到官方,运行命令后会多一步登录谷歌账号校验的步骤,而发布到私有库不需要。

发布到私有库,需要进行私有仓库的搭建,这个网上有很多教程,可以参考下:Flutter私有仓库搭建

注意:

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

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

四、使用

4.1 依赖 pub.dev 仓库的 package 包

cupertino_icons: ^1.0.2

4.2 依赖远程 git 仓库的 package 包

flutter_plugin_name:
git:

url: https://github.com/xxxxxx/xxxxxx.git #git仓库地址

path: xxxxx #如果项目不是在git地址的根目录 则需要指定path

ref: ‘1.0.0' #指定的版本 对应git仓库中的tag标签 也可以指定分支 ref: some-branch

4.3 依赖私有 pub 仓库的 package 包

flutter_plugin_name:
hosted:
name: flutter_plugin_name
url: https://xxxxx

version:1.0.3

4.3 依赖本地仓库的 package 包

flutter_plugin_name:

path: ../ #可以是相对路径也可以是绝对路径

五、其他

在创建项目的时候我们发现还有其他几个类型的 Project type 可选:Application 大家都很熟悉了,它是一个纯 flutter 项目,主体是 Flutter,当然它也可以接入 Android Module 或者 iOS Framework,其内部包含 Android 和 iOS 项目。PluginPackage 上面已经介绍过了,下面来简单看下剩余的几个类型

5.1 Module

项目结构:

特性:

1、这种方式常用与将 Flutter 项目集成到原生项目中进行混合开发,原生项目是主体(宿主)

2、项目中只能使用 Flutter/Dart,不能直接使用原生语言,但是可以使用包含原生语言的 package

3、可以在 pubspec.yaml 使用 package/plugin

适用场景:

1、已有 Native 项目,新开发的功能使用 Flutter 开发,提高效率
2、已存在旧的 Flutter 项目,以 module 方式集成新开发的功能

集成方式:

1、直接使用源码,集成到原生项目中

2、打包成资源包,集成到项目中。如安卓打包成 aar 集成到项目中

存在问题:

1、集成多个 module 时,需要考虑 Flutter Engine 的使用。多 Flutter Engine 会存在内存之间不能共享的问题。Dart 2.15 之后,Isolate 组之内的 isolate 可以共享内存。

2、多个 Flutter engine 会消耗大量资源

3、Native 打开 Flutter 页面时,由于 Flutter Engine 需要初始化,需要消耗一定时间,造成页面跳转存在延迟(“卡顿”)

5.2 Skeleton

这种创建项目方式,从 Flutter 2.5 版本以后开始支持,本质还是一个 Flutter Application,这种方式就是为开发提供一种较好的项目模板,不在是默认的 Couter App。在模板中可以看到路由、assets 资源、多语言、状态管理,功能优先」的文件夹组织方式、主题等多种最佳实践方式

5.3 FFI Plugin

FFI Plugin 是官方提供的一种方式来集成指定平台的本地C源代码功能,虽然常规的 plugin也可以支持,但是主要用途还是支持 method channel,即 dart 调用各种相关平台的 APIAndroid 中的 Java  或  Kotlin APIiOS 中的 Objective-C 或 Swift APIWindows操作系统中的C++ API),而且官方的意思是3.0之后对C源代码功能的支持 FFI Plugin 会更强大,所以我们如果只是调用C代码,不需要平台 SDK API 的话,可以考虑使用 FFI Plugin

创建完项目后,我们观察一下 FFI Plugin 项目的目录结构,对比常规 plugin,主要有以下几点不同:

本地的源代码文件和 CMakeLists.txt 文件现在统一放到项目的 src 目录下,ios 平台目录Classes 下面的源文件存在,只是引入了项目 src 下面的源代码,android 平台 build.gradle 文件中 externalNativeBuild 属性中 cmake 的路径也是指向 src 中的CMakeLists.txt

项目中的 pubspec.yaml 提供了如下配置选项:

代表利用 ffiPlugin 去为各个不同的平台编译源代码,并且绑定了二进制文件集成到 flutter应用中去,你需要哪些平台都需要体现在这个配置项中。

最后通过 DynamicLibrary 来加载库中的方法,具体参考项目 lib 目录下的同名 dart 文件。

六、总结

以上介绍了创建 flutter 不同类型项目的方式、差别与使用场景,并详细介绍了如何开发一个Package 与 Plugin,对应 Package 项目源码与上传 GitHub,感兴趣可参考:

猜你喜欢

转载自blog.csdn.net/you__are_my_sunshine/article/details/133354270