把flutter项目作为aar添加到已有的Android工程上

文章目录

对于已有工程想要尝鲜 Flutter, 很多公司给出了最佳实践方案, android 中是使用 aar 加入项目中, 这样原生开发对于 flutter 环境就没有要求了, 只要 flutter 打包后上传 maven 即可, 但是这部分的过程坑很多, 后面我会再补充这种方案

我也摸索了一个实践方案, 将所有项目的 aar 由 flutter 方打包 aar 后将 aar 置入某一个固定位置 ,并置入一个 git 库管理, 然后 android 原生方直接 pull 后引入项目即可

高能预警: 本篇会结合 flutter, android, aar, gradle, maven, docker 的知识来完成所有的步骤

并不是每一个都会详细说明, 如果有不明白的可以在 的本文下面留言, 我会更新文章或给予解答, 其他渠道的可能不会有时间看

开发环境

本人设备环境

MacOS 10.13.6 (17G65)
flutter: Flutter 1.5.4-hotfix.2 • channel stable

12019-10-25 更新说明: 这篇文章因为发布时效的原因, 当时还没有 `$ flutter build aar` 这个命令
2所以本人并没有实测两个东西的优劣性

预计需要的环境

1xcode
2android sdk
3gradle
4android studio
5flutter sdk
6docker # 这个

这些环境我默认你都有, 没有的话本篇不讲

windows 用户? 对不住, 自己找寻其中的差别吧...

flutter

创建 flutter module

使用命令行创建:

$ flutter create -t module flutter_module

1cd flutter_module
2flutter build apk

这里理论上会生成一个 aar

1tree .android/Flutter/build/outputs
2.android/Flutter/build/outputs
3├── aar
4│   └── flutter-release.aar
5└── logs
6    └── manifest-merger-release-report.txt

嗯,就这个东西

我们其实可以直接把这个 aar 放在宿主中,然后通过配置 aar 本地引用来直接使用这个工程, 但是这样可能并不利于持续集成

所以我们要用到 maven 这个利器

ps: 这里有个坑, 就是纯 flutter 项目可以, 但是如果你的 flutter 项目包含了对于第三方项目的依赖, 则 aar 可能不会包含其他的内容, 我们放在最后面再想办法解决

maven 的处理方式(看看就行,作为错误尝试的步骤)

本篇主要讲的是 maven 的方式, 没有原生 plugin 的很简单, 但是有原生 plugin 的 flutter 步骤过于复杂, 最终没实现, 当然理论上肯定是可以实现的

因为本篇讲解的是本人解决 flutter 附着到已有工程的尝试,所以将放弃的过程也记录下来, 如果你只是想看最终的实现方案可以跳过本篇和后续所有涉及到 maven 的步骤

maven 是一个包管理工具

如果你公司有自己的私服, 则跳过这一章直接看下一章, 我这里只是使用 docker 创建一个 maven 私服环境

使用的镜像是 sonatype/nexus3

配置

可选: $ docker pull sonatype/nexus3

我比较熟悉的有两种方式:

命令行直接运行

1docker run --name test_nexus -d -p 8099:8081 -v /Volumes/Evo512/docker/nexus/nexus-data:/nexus-data sonatype/nexus3

使用 docker-compose

 1version: '2'
 2
 3services:
 4  my-nexus:
 5    image: sonatype/nexus3
 6    ports:
 7      - 8099:8081
 8    networks:
 9      - nexus-net
10    volumes:
11      - /Volumes/Evo512/docker/nexus/nexus-data:/nexus-data
12
13networks:
14  nexus-net:
15    driver: bridge
1docker-compose -d up

使用 docker-compose 就是类似于配置文件的方式

运行

在浏览器打开 http://localhost:8099

登录的用户名密码,默认是 admin admin123

点开 maven, 毛也没有

20190614095229.png

上传 aar

使用 gradle 上传 aar

使用 android studio 打开 flutter_module 下的.android 目录, 经过一顿同步得到的可能是这样的:

20190614111354.png

一片空白毛都没有...

这时候请 close, 重新打开, 现在是这个鬼样子的

20190614111457.png

采用 project 视图模式

20190614111535.png

在.android 下增加一个 gradle 文件,名字自取

比如我的就叫 update_aar.gradle

 1apply plugin: 'maven'
 2
 3def GROUP = 'top.kikt.flutter_lib'
 4def ARTIFACT_ID = 'module_example'
 5def VERSION_NAME = "1.0.0"
 6
 7def SNAPSHOT_REPOSITORY_URL = 'http://localhost:8099/repository/maven-snapshots/'
 8def RELEASE_REPOSITORY_URL = 'http://localhost:8099/repository/maven-releases/'
 9def REPOSITORY_URL = VERSION_NAME.toUpperCase().endsWith("-SNAPSHOT") ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL
10
11
12def NEXUS_USERNAME = 'admin'
13def NEXUS_PASSWORD = 'admin123'
14
15afterEvaluate { project ->
16    uploadArchives {
17        repositories {
18            mavenDeployer {
19                pom.groupId = GROUP
20                pom.artifactId = ARTIFACT_ID
21                pom.version = VERSION_NAME
22                repository(url: REPOSITORY_URL) {
23                    authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD)
24                }
25            }
26        }
27    }
28    task androidJavadocs(type: Javadoc) {
29        source = android.sourceSets.main.java.srcDirs
30        classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
31    }
32    task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
33        classifier = 'javadoc'
34        from androidJavadocs.destinationDir
35    }
36    task androidSourcesJar(type: Jar) {
37        classifier = 'sources'
38        from android.sourceSets.main.java.sourceFiles
39    }
40
41    //解决 JavaDoc 中文注释生成失败的问题
42    tasks.withType(Javadoc) {
43        options.addStringOption('Xdoclint:none', '-quiet')
44        options.addStringOption('encoding', 'UTF-8')
45        options.addStringOption('charSet', 'UTF-8')
46    }
47    artifacts {
48        archives androidSourcesJar
49        archives androidJavadocsJar
50    }
51}

这个文件呢, 就是上传用的 gradle 文件, 来源于

前几个 def 要根据你的 maven 来修改, 包名, 端口, 用户名,密码

接着引入 gradle 文件到项目中

修改: Flutter/build.gradle

1android{
2    /// ....
3}
4
5apply from: "${rootDir.path}/update_aar.gradle"

按照下图点击

img

可能会报错

 111:58:23: Executing task 'uploadArchives'...
 2
 3Executing tasks: [uploadArchives]
 4
 5
 6FAILURE: Build failed with an exception.
 7
 8* Where:
 9Settings file '/Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/settings.gradle' line: 7
10
11* What went wrong:
12A problem occurred evaluating settings 'android_generated'.
13> /Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/Flutter/include_flutter.groovy (/Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/Flutter/include_flutter.groovy)
14
15* Try:
16Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
17
18* Get more help at https://help.gradle.org
19
20BUILD FAILED in 0s
2111:58:23: Task execution finished 'uploadArchives'.

似乎是由于路径不对的原因, 请使用如下的方式修改 setting.gradle:

1// Generated file. Do not edit.
2include ':app'
3
4rootProject.name = 'android_generated'
5setBinding(new Binding([gradle: this]))
6//evaluate(new File('include_flutter.groovy'))
7evaluate(new File("$rootDir.path/include_flutter.groovy"))

同步 gradle 后

接着双击

20190614120303.png

就可以上传成功了

然后打开 nexus 查看: http://localhost:8099/#browse/search/maven

20190614130245.png

20190614130414.png

有显示, 说明这个 aar 上传是成功的

后面再上传更改版本号即可

Android 项目(host)

新建项目

20190614105539.png

引入 maven 依赖

添加仓库

根目录 build.gradle, 根据节点增加一个 maven 仓库:

1allprojects {
2    repositories {
3        google()
4        jcenter()
5        maven {
6            url 'http://localhost:8099/repository/maven-releases/'
7        }
8    }
9}

引入库, 在 nexus 的管理界面里可以查看引用方式:

20190614130617.png

接着在app/build.gradle中修改

1
2dependencies {
3    // ...
4    implementation 'top.kikt.flutter_lib:module_example:1.0.0'
5}

经过 sync 以后,使用 project 视图, 可以找到这个库:

20190614131424.png

编码

新建 MyFlutterActivity.java

 1package top.kikit.androidhost;
 2
 3import android.os.Bundle;
 4
 5import io.flutter.app.FlutterActivity;
 6import io.flutter.plugins.GeneratedPluginRegistrant;
 7
 8/// create 2019-06-14 by cai
 9
10public class MyFlutterActivity extends FlutterActivity {
11
12    @Override
13    protected void onCreate(Bundle savedInstanceState) {
14        super.onCreate(savedInstanceState);
15        GeneratedPluginRegistrant.registerWith(this);
16    }
17}

添加到清单文件

1<application>
2    <activity android:name=".MyFlutterActivity" />
3</application>

修改 MainActivity.java

 1package top.kikit.androidhost;
 2
 3import android.content.Intent;
 4import android.os.Bundle;
 5
 6import androidx.appcompat.app.AppCompatActivity;
 7
 8public class MainActivity extends AppCompatActivity {
 9
10    @Override
11    protected void onCreate(Bundle savedInstanceState) {
12        super.onCreate(savedInstanceState);
13        setContentView(R.layout.activity_main);
14        Flutter.startInitialization(this.getApplicationContext());
15        Intent intent = new Intent(this, MyFlutterActivity.class);
16        startActivity(intent);
17    }
18}

这里模拟一进来直接进 FlutterActivity 的场景

建议你的 Android 同事在合适的时机调用 Flutter.startInitialization(this.getApplicationContext()); 这个是官方给出的初始化 flutter 引擎的代码, 否则首屏可能会慢

运行项目

初次运行可能会报错 提示一个 androidO 什么的玩意

两种方案

  1. minSDK 修改为 26, 这个简直不科学
  2. 在 app/build.gradle 下的 android 节点下增加这个代码
1android{
2    compileOptions {
3        sourceCompatibility 1.8
4        targetCompatibility 1.8
5    }
6}

将源码和目标代码等级都设置为 1.8

嗯 这里插一句, 我的 host 使用的是 androidX, 而 flutter 使用的是 android.support, 所以需要按照 androidX 的迁移流程修改一下, 如果你新建项目的时候勾选了 androidX, 则这里应该不用修改

androidX 的问题可以查看我的, 虽然是 flutter 分类下的,但是对于普通 android 工程也适用

运行结果如下:

20190614133601.png

在 flutter 中添加带有原生功能的库

这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件
这里注意!!!!!!, 请先备份前面几个文件

因为一旦 flutter packages get, 则 前面的文件就木有了

在 flutter 中添加库

这里简单举例一下, 使用一个比较常用的shared_preferences

修改 flutter 的 yaml 文件

1dependencies:
2  shared_preferences: ^0.5.3+1

$ flutter packages get

这一步后, 之前的那几个文件没有了...

建议: 把 build.gradle 和 setting.gradle 复制到 module 级别的某个目录下, 比如叫 template

然后用脚本来做这个上传的事情

  1. 复制模板到对应目录
  2. 通过环境变量设置 aar 的版本号
  3. 使用 gradle 命令来完成插件的调用

上传新版本的 aar

修改版本号为 1.0.1

这里上传成功了

到 android host 中用了一下, 果不其然和网上的朋友们说的一样报错了

 1ERROR: Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT.
 2Show Details
 3Affected Modules: app
 4
 5
 6ERROR: Unable to resolve dependency for ':app@debugAndroidTest/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT.
 7Show Details
 8Affected Modules: app
 9
10
11ERROR: Unable to resolve dependency for ':app@debugUnitTest/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT.
12Show Details
13Affected Modules: app

查看对应的 pom.xml(我这里是 1.0.2),道理是一样的

 1<project xmlns="http://maven.apache.org/POM/4.0.0"
 2    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 3    <modelVersion>4.0.0</modelVersion>
 4    <groupId>top.kikt.flutter_lib</groupId>
 5    <artifactId>module_example</artifactId>
 6    <version>1.0.2</version>
 7    <packaging>aar</packaging>
 8    <dependencies>
 9        <dependency>
10            <groupId>io.flutter.plugins.sharedpreferences</groupId>
11            <artifactId>shared_preferences</artifactId>
12            <version>1.0-SNAPSHOT</version>
13            <scope>compile</scope>
14        </dependency>
15        <dependency>
16            <groupId>com.android.support</groupId>
17            <artifactId>support-v13</artifactId>
18            <version>27.1.1</version>
19            <scope>compile</scope>
20        </dependency>
21        <dependency>
22            <groupId>com.android.support</groupId>
23            <artifactId>support-annotations</artifactId>
24            <version>27.1.1</version>
25            <scope>compile</scope>
26        </dependency>
27    </dependencies>
28</project>

这里有一个 io.flutter.plugins.sharedpreferences 就是报错的元凶了

思考解决方案

看到这里我感觉有如下的方案

  1. 将所有文件打包到同一个 aar 库中, 然后再上传(也就是网上那个 fat-aar 的方案)
  2. 修改 flutter 打包脚本, 然后将中间的三方库产物(sp 插件)上传至私服 maven, flutter 项目使用 api 的方式依赖这些库, 完成 host=>flutter=>other plugin 的目的
  3. 不用 maven, 只用 aar

个人第一感觉, 觉得第一个实施起来可能会简单一些, 先尝试一下

fat-aar

这个找到了两个项目:

一个 gradle 文件的方式:

一个是 plugin 的方式:

但是都要用到一个类似embed这样的关键字来替换 compile(api/implementation), 无奈找遍 gradle 没找到修改的地方, 只能暂时放弃

flutter 的插件库上传至 maven

这个初始来看很可行.. 但仔细一想, 因为那个版本号的作祟, 需要改动的地方不算很少

每个插件包内的 gradle 文件都需要修改:

  1. 修改 version 版本号,这个应该是可以通过 环境变量/gradle 命令 来指定为佳, 不能指定的话理论上和 pub 的版本号相同也可以, 如果是 git 依赖, 就用 ref, path 依赖就很比较难自动取了
  2. 上传脚本,这个要读取上面的版本号, 还要读取一个

为什么要修改版本号呢? flutter 依赖的插件的版本号会被带到 aar 对应的 maven 库中的 pom.xml 文件中

这里要插一句: pom.xml 中依赖的版本号是定义在每个插件自己的 build.gradle 中的,如下面的连接那样

如下所示:

我这里说需要修改的就是这个版本号,否则你上传 maven 的 flutter 库的版本号和插件的 maven 版本号没对上的话,依然会报错

修改版本号并上传需要遵循如下的步骤:

  1. 读取本地.flutter-plugins文件的内容,将其中的版本号字段取出来
  2. 找到插件文件夹,替换掉版本号字段的内容
  3. 将上传插件的脚本复制至对应文件夹,并将版本号,group 名与插件统一
  4. 启动上传脚本
  5. 将对原生文件的修改内容还原

为什么要做最后一步呢? 这种"从远端"镜像下来的东西,修改回去是一个好习惯, 因为修改了会破坏仓库本身版本的完整性

解决方案-使用 aar 和 git 管理

这个就是我开篇说的解决方案, 不使用 maven, 只是打包出 aar, 集中起来, 置入 git 仓库,如果有必要就打 tag 后 push 到远端, 方便根据版本来引用

然后作为 android 原生方, 在 project 的 gradle 中引入 aar 库即可, 当然如果你是大公司有自己的要求, 还是用上一种比较好

git 和 aar 引入也是很成熟的使用方案了, 无非就是如何拼接而已的问题, 何况这一步还可以通过 gradle 自动完成

处理 flutter 端

这次使用 dart 来作为脚本, 毕竟 dart 语言对于 flutter 开发者来说会很熟悉, 当然这一步可以用任何你熟悉的方式,比如: shell/python 等等, 这一步的执行需要将 dart 放入环境变量中

build_module.dart:

 1import 'dart:io';
 2
 3var outputDir = Directory("../output");
 4var targetDir = Directory("../../flutter-aar");
 5
 6Future main() async {
 7  List<AAR> list = [];
 8
 9  outputDir.deleteSync(recursive: true);
10  outputDir.createSync(recursive: true);
11  var file = File("../.flutter-plugins");
12  var plugins = file.readAsLinesSync();
13  for (var value in plugins) {
14    if (value.trim().isEmpty) {
15      continue;
16    }
17    var splitArr = value.split("=");
18    var name = splitArr[0];
19    var path = splitArr[1];
20
21    var aar = handlePlugin(name, path);
22    list.add(aar);
23  }
24
25  var aar = await handleFlutter();
26  list.add(aar);
27
28  handleAAR(list);
29}
30
31void handleAAR(List<AAR> list) {
32  targetDir.deleteSync(recursive: true);
33  targetDir.createSync();
34  list.forEach((aar) {
35    var targetPath = "${targetDir.path}/${aar.aarName}";
36    var targetFile = aar.file.copySync(targetPath);
37    print(
38        '\ncopy "${aar.file.absolute.path}" to "${targetFile.absolute.path}"');
39  });
40}
41
42AAR handlePlugin(String name, String path) {
43  var result = Process.runSync("./gradlew", ["$name:assRel"],
44      workingDirectory: "../.android");
45  print(result.stdout);
46
47  var aarFile = File("$path/android/build/outputs/aar/$name-release.aar");
48  var aarName = aarFile.path.split("/").last;
49  var pathName = "${outputDir.path}/$aarName";
50  var targetFile = aarFile.copySync(pathName);
51  return AAR()
52    ..file = targetFile
53    ..aarName = aarName;
54}
55
56Future<AAR> handleFlutter() async {
57  var processResult = await Process.run(
58    "flutter",
59    ["build", "apk"],
60    workingDirectory: "..",
61    runInShell: true,
62  );
63
64  print(processResult.stdout);
65
66  var name = "flutter-release.aar";
67
68  var file = File("../.android/Flutter/build/outputs/aar/flutter-release.aar");
69  var target = file.copySync("${outputDir.path}/$name");
70
71  return AAR()
72    ..file = target
73    ..aarName = name;
74}
75
76class AAR {
77  String aarName;
78  File file;
79
80  String get noExtensionAarName => aarName.split(".").first;
81
82  @override
83  String toString() {
84    return 'AAR{aarName: $aarName, file: $file, noExtensionAarName: $noExtensionAarName}';
85  }
86}

大概解释下脚本的功能:

  1. 处理.flutter-plugins文件,获取 android 所在目录
  2. 执行flutter/.android下的 gradle 命令来生成 aar
  3. 根据插件所在目录来获取 aar 文件
  4. 打包 flutter 本身的 aar, 这一步因为一些资源的原因, 直接使用 flutter build apk, 会完成所有的中间产物的生成
  5. 将 插件和 flutter 的 aar 文件复制到 output/flutter-aar 文件夹下

output 文件夹就是我们作为 git 依赖使用的文件夹, 这个文件夹

命令: $ dart build_aar.dart

新建一个目录用于存放 aar

因为 git submodule 的管理方式对于新手不友好, 所以使用更简单一点的方案管理

新建一个目录,把所有的 aar 文件都放在一起 (我的示例代码是放在一个仓库里的, 不过是同级目录)

当前的目录结构是这样的:

 1tree -L 2
 2.
 3├── README.md
 4├── android-host
 5│   ├── android-host.iml
 6│   ├── app
 7│   ├── build
 8│   ├── build.gradle
 9│   ├── gradle
10│   ├── gradle.properties
11│   ├── gradlew
12│   ├── gradlew.bat
13│   ├── local.properties
14│   └── settings.gradle
15├── flutter-aar
16│   ├── flutter-release.aar
17│   └── shared_preferences-release.aar
18└── flutter_module
19    ├── README.md
20    ├── build
21    ├── flutter_module.iml
22    ├── flutter_module_android.iml
23    ├── lib
24    ├── output
25    ├── pubspec.lock
26    ├── pubspec.yaml
27    ├── shell
28    ├── template
29    └── test

这样分级的好处是仓库权限的分级:

android 组允许访问 android-host 和 flutter-aar

flutter 组允许访问 flutter_module 和 flutter-aar

我示例代码是一个仓库, 但实际上对于项目来说应该是 3 个仓库为佳

修改 android 主工程

build.gradle:

 1
 2def aarDir = "${rootProject.projectDir.path}/../flutter-aar"
 3
 4repositories {
 5    flatDir {
 6        dirs aarDir
 7    }
 8}
 9
10dependencies {
11    implementation fileTree(dir: 'libs', include: ['*.jar'])
12    implementation 'androidx.appcompat:appcompat:1.0.2'
13    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
14    testImplementation 'junit:junit:4.12'
15    androidTestImplementation 'androidx.test:runner:1.2.0'
16    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
17
18    def file = new File(aarDir)
19    file.listFiles(new FilenameFilter() {
20        @Override
21        boolean accept(File dir, String name) {
22            return name.endsWith("aar")
23        }
24    }).each { f ->
25        def aar = f.name.split("\\.").first()
26        println("f.name = ${f.name} , aar = $aar")
27        api(name: f.name.split("\\.").first(), ext: 'aar')
28    }
29}

这样的情况下这个目录就完成了对于所有 aar 文件的引用

总结一下所有修改

dart 脚本

  1. 复制我提供的仓库下flutter_module/shell/build_module.dart到你的 flutter 下的 shell 目录
  2. 修改这个 dart 脚本中的 targetDir 目录到任何你想要的目录(无论是直接到原生还是到单独仓库内)

原生部分修改

修改 build.gradle 加入对于 aar 的引用

这里使用仓库还是直接在原生工程里看你们项目管理的要求

这一步可以从原生项目的 app/build.gradle 看到所有修改

运行脚本

总结一下我的运行步骤:

  1. 命令行在根目录下执行 cd flutter_module/shell && dart build_module.dart
  2. 运行 android 项目

建议的步骤如下:

对于 flutter 开发者来说:

  1. cd flutter_project/shell && dart build_module.dart
  2. cd android-aar
  3. 操作 git 仓库,上传 aar

对于安卓原生来说:

  1. $ cd android-aar
  2. $ git pull
  3. 运行项目

后记

本篇详细介绍了我是如何解决 flutter 添加到已有工程的方案, 虽然字数多, 但是实际引入并不复杂

可能有遗漏, 有不清楚的请在 下评论留言, csdn 仅作为文章的同步发布平台, 评论可能没有时间看

嗯,仓库在这里:

以上