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目录包含共享代码,其中至少包含一个.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

Flutter开发之Package与Plugin_第1张图片

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

Flutter开发之Package与Plugin_第2张图片

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

Flutter开发之Package与Plugin_第3张图片

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

然后启动单元测试:

Flutter开发之Package与Plugin_第4张图片

如果是涉及 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 materialDialog({
    required BuildContext context,
    Function(dynamic value)? onClose,
    String? title,
    String? msg,
    List? 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? 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 工程了,项目结构如下:

Flutter开发之Package与Plugin_第5张图片

在 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 createState() => _HomePageState();
}

class _HomePageState extends State {
  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"),
    );
  }
}

页面效果如下:

Flutter开发之Package与Plugin_第6张图片Flutter开发之Package与Plugin_第7张图片

二、 插件(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

Flutter开发之Package与Plugin_第8张图片

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

Flutter开发之Package与Plugin_第9张图片

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 getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  Future 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 getPlatformVersion() async {
    final version = await methodChannel.invokeMethod('getPlatformVersion');
    return version;
  }

  @override
  Future getScreenWidth() async {
    final screenWidth = await methodChannel.invokeMethod('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 getPlatformVersion() {
    return FlutterPluginDemoPlatform.instance.getPlatformVersion();
  }

  Future 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()
#import 
#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*)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 createState() => _MyAppState();
}

class _MyAppState extends State {
  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 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 _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),
            ),
          ],
        ),
      ),
    );
  }
}

最终页面效果:

Flutter开发之Package与Plugin_第10张图片Flutter开发之Package与Plugin_第11张图片

三、发布

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

3.1 发布前准备

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

Flutter开发之Package与Plugin_第12张图片

  • pubspec.yaml

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

Flutter开发之Package与Plugin_第13张图片

  • README.md

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

Flutter开发之Package与Plugin_第14张图片

  • CHANGELOG.md

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

Flutter开发之Package与Plugin_第15张图片

  • LICENSE

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

Flutter开发之Package与Plugin_第16张图片

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

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 上面已经介绍过了,下面来简单看下剩余的几个类型

Flutter开发之Package与Plugin_第17张图片

5.1 Module

项目结构:

Flutter开发之Package与Plugin_第18张图片

特性:

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开发之Package与Plugin_第19张图片

这种创建项目方式,从 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

Flutter开发之Package与Plugin_第20张图片

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

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

Flutter开发之Package与Plugin_第21张图片

Flutter开发之Package与Plugin_第22张图片

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

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

Flutter开发之Package与Plugin_第23张图片

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

六、总结

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

  • flutter_package_demo:GitHub - ericwangjp/flutter_package_demo: A new flutter package demo project.
  • flutter_plugin_demo:https://github.com/ericwangjp/flutter_plugin_demo

你可能感兴趣的:(flutter开发,flutter)