前言

本篇写一个将 flutter 打包为 aar 置入已有项目的方案

前篇不同的是: 本篇使用新版本的 flutter 环境, 使用 build aar 命令构建 aar,并上传至 maven 私服

开发环境

$ flutter doctor -v
[✓] Flutter (Channel stable, v1.12.13+hotfix.7, on Mac OS X 10.15 19A602, locale zh-Hans-CN)
    • Flutter version 1.12.13+hotfix.7 at /Users/caijinglong/Library/Flutter/flutter_dev
    • Framework revision 9f5ff2306b (9 天前), 2020-01-26 22:38:26 -0800
    • Engine revision a67792536c
    • Dart version 2.7.0

准备步骤

创建宿主工程

这个是模拟你本来的项目

作为原生开发者自行使用 Android Studio 创建即可

image

一个标准的 android 项目, 除了 gradle 版本使用 6.1.1, 和 maven 仓库使用阿里云镜像, 其他并没有什么修改

创建 flutter 项目

这个是模拟你的 flutter 项目

使用 module type

$ flutter create -t module flutter_module_example

image

如果你不是这种类型创建的项目, 理论上也可以用 flutter build aar, 但是不保证完全一样

本机启动一个 maven 私服

这里使用 docker-compose 启动, 根据你自己的情况来安装, 如果你有公司的 maven 私服或你不喜欢用 docker, 则根据你自己的实际情况跳过这步或使用别的 maven 私服

image

version: "3"

services:
  nexus:
    image: sonatype/nexus3
    ports:
      - 9000:8081
    volumes:
      - /Volumes/Samsung-T5/docker/nexus/data:/nexus-data

语法是 宿主机:docker 镜像

端口是将 docker 的 8081 映射到本机的 9000 端口

volumes 是映射 nexus 的数据到本机的文件夹内

$ docker-compose up 启动 docker

image

我这里不是初次安装, 所以没有, 你如果是初次安装可以从 data 文件夹内找到 admin.password 查看初始密码

一般初次登陆会让你设置一个, 记住它后面要用到

image

这个就是私服的 maven url 地址

flutter 项目

修改项目

本篇修改一下 flutter 项目, 引入一个path_provider

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  path_provider: any

实际开发时别用这个 any 版本号, 我只是懒得看最新版是多少而已

构建 aar

$ flutter build aar 来打包

image

这里 build 完, console 日志有提示你怎么配置你的 host 项目, 如果你是自己开发, 直接用本地的 repo 目录作为仓库就可以了, 将配置复制到项目里即可

repositories 闭包配置可以写在在 host 项目, 也可以配置在 app module 中, 你如果不明白项目和 module 的差别, 可以请教你们安卓原生同事, 也可以自行百度

其他的配置请配置到 app module 里


// 添加 maven 仓库 url
repositories {
    maven {
        url '/Volumes/Samsung-T5/code/flutter/exists_blog/flutter_module_example/build/host/outputs/repo' // 这个是我本地的, 你根据你自己的情况肯定不一样, 不要直接复制粘贴我的, 粘贴命令行里的
    }
    maven {
        url 'http://download.flutter.io' // 这个是flutter的maven仓库地址
    }
}

// 添加依赖
dependencies {
    debugImplementation 'com.example.flutter_module_example:flutter_debug:1.0'
    profileImplementation 'com.example.flutter_module_example:flutter_profile:1.0'
    releaseImplementation 'com.example.flutter_module_example:flutter_release:1.0'
}

// 因为android项目默认是没有profile类型的, 这里需要你自己加一个, 然后配置参考debug
android {
    buildTypes {
        profile {
            initWith debug
        }
    }
}

当然, 实际上不太可能是自己开发, 即便是自己开发, 也不可能家里和公司连目录结构都一模一样, 然后这里就是 maven 私服的作用了, 让依赖相同, 以便于合作开发

分析下构建产物

这里我们注意到, 我们依赖只有一个 flutter_module_example, 没有 path_provider, 这个就是 maven 帮我们处理的了

image

看目录结构, path_provider 是有的, 还分为 release profile debug

image

这里是 flutter 项目对应的 pom 文件, 依赖了三部分代码, 这样 maven 就会自动帮你把这三部分代码都下载并依赖

上传依赖

使用界面上传

image

image

这里分别上传就看见了

image

因为是演示, 我只上传 release 的, 然后在 host 项目里引用

项目里, 根目录的build.gradle

repositories{
    maven{
        url 'http://localhost:9000/repository/maven-releases/' // 你自己的maven私服地址
    }
    maven{
        url 'http://download.flutter.io'
    }
}

app 的build.gradle

dependencies{
    implementation 'com.example.flutter_module_example:flutter_release:1.0'
}

这样就可以了

当然肯定还有问题, 但是基本的思路已经通了, 接着就是怎么自动化的问题了

自动化的探索

命令行上传

自动化的话, 肯定需要有工具或者命令行上传

我这里选择的是命令行的方案

但在这之前, 我需要先把旧的包删除掉, 以便于保证我命令行的包可以上传成功

image

在 browser 视图中统统删掉

接着进入刚刚的 repo 文件夹

$ cd flutter_module_example/build/host/outputs/repo/com/example/flutter_module_example/flutter_release/1.0

根据maven 官网说明 可以使用 mvn deploy:deploy-file 命令来上传

$ mvn deploy:deploy-file -DpomFile=flutter_release-1.0.pom -Dfile=flutter_release-1.0.aar -Durl=http://localhost:9000/repository/maven-releases/

image

这里我得到了一个错误, 错误原因呢是认证失败, 也就是说没权限, 这是肯定的, 因为一个 maven 私服你能看就不错了, 没权限还想上传怕是想太多, 万一你给人挂个马怎么办?

所以我们需要配置权限

眼尖的同学可能发现了, 有一个 Help 1 标签告诉我们如何配置权限

点开一看, 有 3 种方式:

  1. 配置 http 代理, 然后在代理里帮我们加上 basic 的请求头(这里就引申出一个, 可以用反代的方式, 你请求自己的某个 nginx/caddy, 然后由 nginx 帮你加 http 的 authentication 请求头, 但是这样做太复杂没必要)
  2. settings.xml 中针对仓库配置密码或私钥选项
  3. 配置 ssl 来连接

这里 ssl 比较复杂, 不适合 demo 演示, 如果你们公司有要求则根据你们公司的来

我这里选用 settings.xml 来做, 并且为了演示方便, 我将 settings 放在项目文件夹下, 然后用mvn -s 参数引用配置文件, 这样的好处是对于系统的 mvn 配置没有侵入性, 当然你也可以按照官方的说法修改 m2 文件夹下的配置文件,这个请自行搜索如何做

我的 maven 是 brew 安装的, 可以通过brew info maven来查看

然后进入到<maven>/libexec/conf文件夹下, 复制 settings.xml 出来

然后找到 servers 标签修改如下:

<servers>
    <server>
      <id>nexus</id>
      <username>admin</username>
      <password>admin</password>
    </server>
<servers>

用户名,密码换成你的

mvn deploy:deploy-file \
-DpomFile="build/host/outputs/repo/com/example/flutter_module_example/flutter_release/1.0/flutter_release-1.0.pom" \
-DgeneratePom=false \
-Dfile="build/host/outputs/repo/com/example/flutter_module_example/flutter_release/1.0/flutter_release-1.0.aar" \
-Durl="http://localhost:9000/repository/maven-releases" \
-DrepositoryId="nexus" \
-Dpackaging=aar \
-s="mvn-settings.xml"

这样就可以提交成功了

但是这样会发现一个问题, flutter 打包出来的版本号永远是 1.0, 而 flutter 在稍后的版本提供了参数可以在 build aar 的时候修改版本号, 而对于目前应用广泛的 stable 版(1.12.13+hotfix.7)来说, 并没有这个功能

所以, 我们需要在上传前修改 pom 文件, 将版本号根据某个规律提升或指定, 而这个是可以自动化完成的事情, 因为 pom 实质上就是 xml 文件, 而 xml 的解析对于大部分语言来说都是有三方库可以完成的

整理思路

自动化部署需要完成如下的步骤

  1. flutter build aar
  2. 找到所有的 aar 文件对应的 pom 文件, 使用 xml 解析并将版本号修改, 同时修改依赖对应的版本号
  3. 上传 aar 和 pom
  4. 修改 android 端的依赖到最新的 flutter 版本号

编写脚本

思路有了, 我们编写工具完成这个步骤就可以了, 这里我使用 dart 来完成, 因为 dart 是 flutter 的开发语言, 对于 flutter 开发来说上手难度较低, 当然也可以用 python/go 等任何你熟悉的语言来完成

import 'dart:convert';
import 'dart:io';
import 'package:xml/xml.dart' as xml;

const targetVersion = "1.0.4";

class DeployObject {
  File pomFile;
  File aarFile;
}

void main() {
  List<File> aarFiles = [];
  List<String> needChangeList = [];
  List<DeployObject> deploys = [];

  final dir = Directory("build/host/outputs/repo");

  // 扫描aar
  for (final file in dir.listSync(recursive: true)) {
    if (file.path.endsWith(".aar")) {
      aarFiles.add(file); // aar文件
      needChangeList.add(
        file.uri.pathSegments[file.uri.pathSegments.length - 3],
      ); // 库的名称, 为了简单, 我只扫描了项目名称, 没有扫描组名, 如果你不幸有重名, 需要自己增加对于包名的处理
    }
  }

  for (final aar in aarFiles) {
    final pomFile = handlePom(needChangeList, aar); // 处理pom文件
    deploys.add(DeployObject()
      ..aarFile = aar
      ..pomFile = pomFile);
  }

  for (final deploy in deploys) {
    deployPkt(deploy);
  }
}

File handlePom(List<String> needChangeVersionList, File aarFile) {
  final pomPath = aarFile.path.substring(0, aarFile.path.length - 3) + 'pom';

  final file = File(pomPath);

  final doc = xml.parse(file.readAsStringSync());

  {
    // 修改自身的版本号
    final xml.XmlText versionNode =
        doc.findAllElements("version").first.firstChild;
    versionNode.text = targetVersion;

    // 这里添加大括号只是为了看的清楚, 实际可不加
  }

  final elements = doc.findAllElements("dependency");
  for (final element in elements) {
    final artifactId = element.findElements("artifactId").first.text;
    if (needChangeVersionList.contains(artifactId)) {
      final xml.XmlText versionNode =
          element.findElements("version").first.firstChild;
      versionNode.text = targetVersion; // 修改依赖的版本号
    }
  }

  final buffer = StringBuffer();

  doc.writePrettyTo(buffer, 0, "  ");
  print(buffer);

  return file..writeAsStringSync(buffer.toString());
}

Future<void> deployPkt(DeployObject deploy) async {
  final configPath = File('mvn-settings.xml').absolute.path;
  List<String> args = [
    'deploy:deploy-file',
    '-DpomFile="${deploy.pomFile.absolute.path}"',
    '-DgeneratePom=false',
    '-Dfile="${deploy.aarFile.absolute.path}"',
    '-Durl="http://localhost:9000/repository/maven-releases"',
    '-DrepositoryId="nexus"',
    '-Dpackaging=aar',
    '-s="$configPath"',
  ];
  final shell = "mvn ${args.join(' \\\n    ')}";
  final f = File(
      "${Directory.systemTemp.path}/${DateTime.now().millisecondsSinceEpoch}.sh");
  f.writeAsStringSync(shell);
  final process = await Process.start('bash', [f.path]);
  final output = await utf8.decodeStream(process.stdout);
  print(output);
  final exitCode = await process.exitCode;
  if (exitCode != 0) {
    exit(exitCode);
  }
}

完整版的脚本在这里 只保证有 bash 环境可用, 因为是用 bash 来执行的, window 可能需要根据你的环境有所修改

然后, 每次都会创建 n 个临时的 sh 文件用于上传

我这里执行一下: $ dart bin/main.dart

image

能看到一堆的执行成功

image

然后我们查看 nexus 的 web 端, 我们能看到 1.0.4 在 maven 私服上已经有了

点右边的 pom 文件的连接看看内容

image

自身是 1.0.4 版本, 依赖的 path_provider 也是, 但 io.flutter 的引擎体系还是应该的版本号

宿主项目的修改

接着就是修改宿主项目的依赖了, 我们最早的时候用的是 1.0 版本

我们添加依赖

implementation 'com.example.flutter_module_example:flutter_release:1.0.4'

然后 gradle sync, 等待一会儿就能同步成功了

这样以后我们 flutter 开发人员就可以直接对安卓组说, 我更新了一个开发版, 1.0.5-dev1, 你试试能不能跑, 交互有没有问题之类的

修改代码打开 flutter 的页面

这里就是修改 MainActivity 了

修改布局文件: activity_main.xml

image

修改下右边的 text 属性

修改 java 文件

package top.kikt.flutterhost;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;

import io.flutter.app.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.view.FlutterMain;

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // 添加下面那段
    findViewById(R.id.bt_to_flutter).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        FlutterMain.startInitialization(getApplicationContext());
        FlutterMain.ensureInitializationComplete(getApplicationContext(), null);
        Intent intent = new Intent(MainActivity.this, FlutterActivity.class);
        startActivity(intent);
      }
    });
  }
}

开启新页面的代码, 一点, 嗯.. crash 了

没事, 我们查查日志

image

哦, 原来是没添加 Activity 到 manifest 里, 好久没写安卓都忘了

打开AndroidManifest.xml

<application>
 <activity android:name="io.flutter.app.FlutterActivity" />
</application>

然后运行就可以跑起来了, 点下 Open flutter 按钮

image

image

开启 flutter 页面的时候会黑屏一下, 这是因为这里需要加载 flutter 引擎, 提前加载引擎可以减少这种情况, 但无法根治, 请自行百度搜索解决方案

这里是提前初始化的代码

package top.kikt.flutterhost;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;

import io.flutter.app.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.view.FlutterMain;

public class MainActivity extends AppCompatActivity {

  boolean flutterInited = false;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    findViewById(R.id.bt_to_flutter).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        if (!flutterInited) {
          FlutterMain.startInitialization(getApplicationContext());
          FlutterMain.ensureInitializationComplete(getApplicationContext(), null);
        }
        Intent intent = new Intent(MainActivity.this, FlutterActivity.class);
        startActivity(intent);
      }
    });

    initFlutterEngine();
  }

  void initFlutterEngine() {
    FlutterMain.startInitialization(getApplicationContext());
    FlutterMain.ensureInitializationCompleteAsync(getApplicationContext(), null, new Handler(), new Runnable() {
      @Override
      public void run() {
        flutterInited = true;
      }
    });
  }
}

这里如果有用模拟器的朋友可能会遇到没找到 so 的 crash, 这是因为 x86 引擎没有 release 的 so 的原因, 而我们是单独引用 release 的原因

我们按照官方的方案添加如下配置在 app 的 build.gradle 里

android{
    buildTypes {
        profile {
            initWith debug
        }
    }
}

dependencies{
    def flutterModuleVersion = '1.0.4'
    debugImplementation "com.example.flutter_module_example:flutter_debug:$flutterModuleVersion"
    profileImplementation "com.example.flutter_module_example:flutter_profile:$flutterModuleVersion"
    releaseImplementation "com.example.flutter_module_example:flutter_release:$flutterModuleVersion"
}

这样就可以了

后记

本篇完成了打包 aar 并上传 maven 的全过程

仓库地址

有问题请在博客下留言, 其他地方的留言不保证能看到

以上