基于Flutter1.12.13混合开发Android自动打包dart脚本

1,102 阅读5分钟

flutter 版本:v1.12.13+hotfix.9

测试系统 macOS

原文链接

前言

自从 Flutter1.9.1 稳定版升级到 1.12.13 稳定版后,打包也出现了一些变化,之前使用 fat-aar 后能够把所有的 Flutter 内容全部打包进一个 aar 中,上传到公司 maven 库,Android 开发直接 implementation 对应 aar 就能够使用 Flutter 的相关内容及资源了,升级到 1.12.13 后,Flutter 将与 Flutter 的核心库放到了云上,这会导致 Flutter 使用 fat-aar 打出来的包不可使用 Flutter 相关资源,例如 FlutterEngine。如何在 1.12.13 版本下更好的自动化构建及 Android 更加友好的集成,下面是我的一些思路。

flutter build aar?

升级 1.12.13 后,官方提供了flutter build aar命令来打出 aar 资源,对应的使用方式官方有比较详细的流程,flutter build aar 流程

官方提供的方案使用flutter build aar打包后,会在 build 文件夹下把各个插件资源分别打入不同的文件夹,每个文件夹都会有对应的 aar 及一些信息,我们可以把这个资源全部上传到 maven,这个时候 maven 库可能是这样

maven目录截图

我也尝试过使用官方打出的 aar 上传到 maven,但这会导致两个问题:

  1. 上传多个文件将会非常慢,严重影响打包时间
  2. Android 开发人员下拉 Flutter 相关 aar 也会很慢,而且有很大概率会超时

优化

原理:

  1. 通过引入 fat-aar,把除了 flutter 核心模块外的模块打成一个 aar
  2. 通过上传 maven 所使用的 pom 文件将 flutter 核心模块写入到 pom 文件中,大概是这个样子
<dependency>
  <groupId>io.flutter</groupId>
  <artifactId>flutter_embedding_release</artifactId>
  <version>1.0.0-af51afceb8886cc11e25047523c4e0c7e1f5d408</version>
  <scope>compile</scope>
</dependency>
  1. Android 客户端此方式引入api 'xxx:0.0.1'(原来没升级前引入方式是api 'xxx:0.0.1@aar'),在allprojects.repositories中添加
maven {
    url 'http://download.flutter.io'
}

及自己 maven 对应的 url

脚本开发

  1. 封装执行 sh 脚本代码,打印执行 sh 命令的信息
//执行sh脚本
Future<int> start(String executable, List<String> arguments,
    {String workingDirectory,
    Map<String, String> environment,
    bool includeParentEnvironment = true,
    bool runInShell = true,
    ProcessStartMode mode = ProcessStartMode.normal}) async {
  Process result = await Process.start(executable, arguments,
      workingDirectory: workingDirectory,
      environment: environment,
      includeParentEnvironment: includeParentEnvironment,
      runInShell: runInShell,
      mode: mode);
  result.stdout.listen((out) {
    if (isPrint) print(utf8.decode(out));
  });
  result.stderr.listen((err) {
    print(utf8.decode(err));
  });
  return result.exitCode;
}
  1. 执行upgrade拉取最新插件信息
await start('flutter', ['pub', 'upgrade']);
  1. 复制及插入 fat-aar 相关代码及文件
print('复制打包aar相关文件');
  {
    File copyFile =
        File(projectDir + '.android/config/dependencies_gradle_plugin.gradle');
    copyFile.createSync(recursive: true);
    copyFile.writeAsStringSync(gradlePlugin);
  }
  print('插入fat-aar相关的包');
  {
    print('插入文件到.andriod/build.gradle');
    File file = File(projectDir + '/.android/build.gradle');
    String buildGradle = file.readAsStringSync();
    if (!buildGradle.contains('com.kezong:fat-aar')) {
      buildGradle = buildGradle.replaceAllMapped(
          RegExp(r"'com.android.tools.build:gradle\S+"), (group) {
        return group.groups([0])[0] + '\nclasspath "com.kezong:fat-aar:1.2.12"';
      });
      file.writeAsStringSync(buildGradle);
    }

    print('插入文件到.andorid/Flutter/build.gradle');
    File flutterGradleFile =
        File(projectDir + '/.android/Flutter/build.gradle');
    String flutterGradleStr = flutterGradleFile.readAsStringSync();
    if (!flutterGradleStr.contains('com.kezong.fat-aar')) {
      flutterGradleStr +=
          '\napply plugin: "com.kezong.fat-aar"\napply from: "../config/dependencies_gradle_plugin.gradle"';
      flutterGradleFile.writeAsStringSync(flutterGradleStr);
    }
  }

其中 gradlePlugin 是一个 gradle 模板

//加载aar的gradle
String gradlePlugin = """
  dependencies {
    def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
    def plugins = new Properties()
    def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
    if (pluginsFile.exists()) {
        pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
    }
    plugins.each { name, path->
        File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
        println name
        if (editableAndroidProject.exists()) {
            embed project(path: ":\$name", configuration: 'default')
        }
    }
}
""";
  1. 开始打包
print('开始打包');
  {
    int exitCode;
    print('正在打包...');
    if (buildMode == 'debug') {
      exitCode = await start('bash', ['./gradlew', 'assembleProfile'],
          workingDirectory: '$projectDir/.android');
    } else if (buildMode == 'release') {
      exitCode = await start('bash', ['./gradlew', 'assembleRelease'],
          workingDirectory: '$projectDir/.android');
    } else {
      exitCode = await start('bash', ['./gradlew', 'assemble'],
          workingDirectory: '$projectDir/.android');
    }
    if (exitCode != 0) {
      print('打包出错');
      return false;
    } else {
      print('打包成功');
    }
  }

这个时候可以打开.android/Flutter/build/outputs/aar/看到相关的 aar 文件,这里我打 debug 包使用 profile 打包方式,主要原因是我使用的com.kezong:fat-aar插件打出的 debug 包没有把资源打进去,我不清楚这是 bug 还是我使用的方式不对,有人知道可以告诉我一下。

  1. 上传 maven
print('开始上传maven');
  {
    bool result;
    if (buildMode == 'debug' || buildMode == 'release') {
      result = await uploadMaven(buildVersion, buildMode);
      await uploadToWX(buildVersion, buildMode, exitCode == 0);
      if (!result) {
        print('上传aar失败');
        return false;
      }
    } else {
      result = await uploadMaven(buildVersion, 'debug');
      await uploadToWX(buildVersion, 'debug', exitCode == 0);
      if (exitCode != 0) {
        print('上传aar失败');
        return false;
      }
      result = await uploadMaven(buildVersion, 'release');
      await uploadToWX(buildVersion, 'release', exitCode == 0);
      if (exitCode != 0) {
        print('上传aar失败');
        return false;
      }
    }
  }

其中 uploadMaven 方法是专门用来处理上传 maven 有关的文件及替换版本号等

///上传到maven
Future<bool> uploadMaven(String version, String buildMode) async {
  if (mavenUrl == 'xxx') {
    print('你什么都没改,别传maven了');
    return false;
  }
  String pomName; //pom名
  String build; //arr对应文件名
  // final dir = Directory("build/host/outputs/repo");
  if (buildMode == 'debug') {
    pomName = 'trojanDebug';
    build = 'profile';
  } else {
    pomName = 'trojan';
    build = 'release';
  }
  final aar =
      File("$projectDir/.android/Flutter/build/outputs/aar/flutter-$build.aar");
  final pom1 = File("$projectDir/configs/trojan.pom");
  final tempPath = File("${Directory.systemTemp.path}/temp.pom");
  tempPath.createSync(recursive: true);
  final temp = pom1.copySync(tempPath.path);
  print('修改pom内容');
  {
    final doc = xml.parse(temp.readAsStringSync());
    {
      // 修改自身的版本号
      final xml.XmlText versionNode =
          doc.findAllElements("version").first.firstChild;
      versionNode.text = version;
      final xml.XmlText name =
          doc.findAllElements("artifactId").first.firstChild;
      name.text = pomName;
    }
    final elements = doc.findAllElements("dependency");
    // 修改flutter相关依赖名
    for (final element in elements) {
      xml.XmlText artifactId =
          element.findElements("artifactId").first.firstChild;
      if (artifactId.text.contains('release')) {
        artifactId.text =
            artifactId.text.replaceAll('release', build); // 修改依赖的版本号
        print(artifactId);
      }
    }
    final buffer = StringBuffer();
    doc.writePrettyTo(buffer, 0, "  ");
    temp.writeAsStringSync(buffer.toString());
  }
  DeployObject deploy = DeployObject()
    ..aarFile = aar
    ..pomFile = temp;

  //把用户名密码写入mvn-setting.xml
  {
    File setting = File('$projectDir/configs/mvn-settings.xml');
    final doc = xml.parse(setting.readAsStringSync());
    final xml.XmlText user = doc.findAllElements("username").first.firstChild;
    user.text = username;
    final xml.XmlText pwd = doc.findAllElements('password').first.firstChild;
    pwd.text = password;
    final buffer = StringBuffer();
    doc.writePrettyTo(buffer, 0, "  ");
    setting.writeAsStringSync(buffer.toString());
  }
  print('开始上传');
  {
    final configPath =
        File('$projectDir/configs/mvn-settings.xml').absolute.path;
    List<String> args = [
      'deploy:deploy-file',
      '-DpomFile="${deploy.pomFile.absolute.path}"',
      '-Dmaven.metadata.legacy=true',
      '-DgeneratePom=false',
      '-Dfile="${deploy.aarFile.absolute.path}"',
      '-Durl="${mavenUrl}"',
      '-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 exitCode = await start('bash', [f.path]);
    f.deleteSync();
    if (exitCode != 0) {
      print(deploy.aarFile.path.split('/').last + '上传失败');
      return false;
    } else {
      print(deploy.aarFile.path.split('/').last + '上传成功');
    }
  }
  temp.deleteSync();
  return true;
}

其中的build.pom文件可以点击这里看到,主要功能是管理 aar 版本号及存放 flutter 相关的库,这里这些库我是执行flutter build aar拿到的,目前我这个版本生成的都是这几个文件。

我是通过 mvn 命令上传到 maven 的,所以需要安装 mvn 命令去官网安装

最后上传到 maven 仓库后是这样

全部代码请移步flutter_build

目前问题

  1. 我不清楚是否 flutter 的资源名后缀能否自动获取,目前是写死的,比如1.0.0-af51afceb8886cc11e25047523c4e0c7e1f5d408
  2. fat-aar 能否把 flutter 资源下载后打入到一个 aar 中

总结

由于对 gradle 还不是很熟悉,还不清楚是否有更好的打包及集成方式,希望能得到大家提醒,我也能够改进打包脚本