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

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

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

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

开发环境

本人设备环境

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

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

预计需要的环境

xcode
android sdk
gradle
android studio
flutter sdk
docker # 这个
Bash

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

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

flutter

创建 flutter module

使用命令行创建:

$ flutter create -t module flutter_module

cd flutter_module
flutter build apk
Bash

这里理论上会生成一个 aar

tree .android/Flutter/build/outputs
.android/Flutter/build/outputs
├── aar
│   └── flutter-release.aar
└── logs
    └── manifest-merger-release-report.txt
Bash

嗯,就这个东西

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

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

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

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

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

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

maven 是一个包管理工具

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

使用的镜像是 sonatype/nexus3

配置

可选: $ docker pull sonatype/nexus3

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

命令行直接运行

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

使用 docker-compose

version: '2'

services:
  my-nexus:
    image: sonatype/nexus3
    ports:
      - 8099:8081
    networks:
      - nexus-net
    volumes:
      - /Volumes/Evo512/docker/nexus/nexus-data:/nexus-data

networks:
  nexus-net:
    driver: bridge
YAML
docker-compose -d up
Bash

使用 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

apply plugin: 'maven'

def GROUP = 'top.kikt.flutter_lib'
def ARTIFACT_ID = 'module_example'
def VERSION_NAME = "1.0.0"

def SNAPSHOT_REPOSITORY_URL = 'http://localhost:8099/repository/maven-snapshots/'
def RELEASE_REPOSITORY_URL = 'http://localhost:8099/repository/maven-releases/'
def REPOSITORY_URL = VERSION_NAME.toUpperCase().endsWith("-SNAPSHOT") ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL


def NEXUS_USERNAME = 'admin'
def NEXUS_PASSWORD = 'admin123'

afterEvaluate { project ->
    uploadArchives {
        repositories {
            mavenDeployer {
                pom.groupId = GROUP
                pom.artifactId = ARTIFACT_ID
                pom.version = VERSION_NAME
                repository(url: REPOSITORY_URL) {
                    authentication(userName: NEXUS_USERNAME, password: NEXUS_PASSWORD)
                }
            }
        }
    }
    task androidJavadocs(type: Javadoc) {
        source = android.sourceSets.main.java.srcDirs
        classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
    }
    task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
        classifier = 'javadoc'
        from androidJavadocs.destinationDir
    }
    task androidSourcesJar(type: Jar) {
        classifier = 'sources'
        from android.sourceSets.main.java.sourceFiles
    }

    //解决 JavaDoc 中文注释生成失败的问题
    tasks.withType(Javadoc) {
        options.addStringOption('Xdoclint:none', '-quiet')
        options.addStringOption('encoding', 'UTF-8')
        options.addStringOption('charSet', 'UTF-8')
    }
    artifacts {
        archives androidSourcesJar
        archives androidJavadocsJar
    }
}
Groovy

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

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

接着引入 gradle 文件到项目中

修改: Flutter/build.gradle

android{
    /// ....
}

apply from: "${rootDir.path}/update_aar.gradle"
Groovy

按照下图点击img

可能会报错

11:58:23: Executing task 'uploadArchives'...

Executing tasks: [uploadArchives]


FAILURE: Build failed with an exception.

* Where:
Settings file '/Volumes/Evo512/code/flutter/add_to_exists_android/flutter_module/.android/settings.gradle' line: 7

* What went wrong:
A problem occurred evaluating settings 'android_generated'.
> /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)

* Try:
Run 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.

* Get more help at https://help.gradle.org

BUILD FAILED in 0s
11:58:23: Task execution finished 'uploadArchives'.

Bash

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

// Generated file. Do not edit.
include ':app'

rootProject.name = 'android_generated'
setBinding(new Binding([gradle: this]))
//evaluate(new File('include_flutter.groovy'))
evaluate(new File("$rootDir.path/include_flutter.groovy"))
Groovy

同步 gradle 后

接着双击20190614120303.png

就可以上传成功了

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

20190614130245.png

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

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

Android 项目(host)

新建项目

20190614105539.png

引入 maven 依赖

添加仓库

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

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url 'http://localhost:8099/repository/maven-releases/'
        }
    }
}
Groovy

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

20190614130617.png

接着在app/build.gradle中修改


dependencies {
    // ...
    implementation 'top.kikt.flutter_lib:module_example:1.0.0'
}

Groovy

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

20190614131424.png

编码

新建 MyFlutterActivity.java

package top.kikit.androidhost;

import android.os.Bundle;

import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;

/// create 2019-06-14 by cai

public class MyFlutterActivity extends FlutterActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);
    }
}

Java

添加到清单文件

<application>
    <activity android:name=".MyFlutterActivity" />
</application>
XML

修改 MainActivity.java

package top.kikit.androidhost;

import android.content.Intent;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    
Dart

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

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

运行项目

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

两种方案

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

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

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

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

运行结果如下:20190614133601.png

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

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

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

在 flutter 中添加库

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

修改 flutter 的 yaml 文件

dependencies:
  shared_preferences: ^0.5.3+1
YAML

$ flutter packages get

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

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

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

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

上传新版本的 aar

修改版本号为 1.0.1

这里上传成功了

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

ERROR: Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT.
Show Details
Affected Modules: app


ERROR: Unable to resolve dependency for ':app@debugAndroidTest/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT.
Show Details
Affected Modules: app


ERROR: Unable to resolve dependency for ':app@debugUnitTest/compileClasspath': Could not resolve io.flutter.plugins.sharedpreferences:shared_preferences:1.0-SNAPSHOT.
Show Details
Affected Modules: app

Bash

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

<project xmlns="http://maven.apache.org/POM/4.0.0"
    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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>top.kikt.flutter_lib</groupId>
    <artifactId>module_example</artifactId>
    <version>1.0.2</version>
    <packaging>aar</packaging>
    <dependencies>
        <dependency>
            <groupId>io.flutter.plugins.sharedpreferences</groupId>
            <artifactId>shared_preferences</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.android.support</groupId>
            <artifactId>support-v13</artifactId>
            <version>27.1.1</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.android.support</groupId>
            <artifactId>support-annotations</artifactId>
            <version>27.1.1</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
XML

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

思考解决方案

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

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

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

fat-aar

这个找到了两个项目:

一个 gradle 文件的方式: https://github.com/adwiv/android-fat-aar

一个是 plugin 的方式: https://github.com/Vigi0303/fat-aar-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 中的,如下面的连接那样

如下所示: https://github.com/OpenFlutter/flutter_image_compress/blob/e841181d16df44b94c45e77ee1dcd36ebdc27905/android/build.gradle#L1-L2

https://github.com/flutter/plugins/blob/e9766e668b4a84ac526414e26981a23c661aff18/packages/shared_preferences/android/build.gradle#L14-L15

我这里说需要修改的就是这个版本号,否则你上传 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:

import 'dart:io';

var outputDir = Directory("../output");
var targetDir = Directory("../../flutter-aar");

Future main() async {
  List<AAR> list = [];

  outputDir.deleteSync(recursive: true);
  outputDir.createSync(recursive: true);
  var file = File("../.flutter-plugins");
  var plugins = file.readAsLinesSync();
  for (var value in plugins) {
    if (value.trim().isEmpty) {
      continue;
    }
    var splitArr = value.split("=");
    var name = splitArr[0];
    var path = splitArr[1];

    var aar = handlePlugin(name, path);
    list.add(aar);
  }

  var aar = await handleFlutter();
  list.add(aar);

  handleAAR(list);
}

void handleAAR(List<AAR> list) {
  targetDir.deleteSync(recursive: true);
  targetDir.createSync();
  list.forEach((aar) {
    var targetPath = "${targetDir.path}/${aar.aarName}";
    var targetFile = aar.file.copySync(targetPath);
    print(
        '\ncopy "${aar.file.absolute.path}" to "${targetFile.absolute.path}"');
  });
}

AAR handlePlugin(String name, String path) {
  var result = Process.runSync("./gradlew", ["$name:assRel"],
      workingDirectory: "../.android");
  print(result.stdout);

  var aarFile = File("$path/android/build/outputs/aar/$name-release.aar");
  var aarName = aarFile.path.split("/").last;
  var pathName = "${outputDir.path}/$aarName";
  var targetFile = aarFile.copySync(pathName);
  return AAR()
    ..file = targetFile
    ..aarName = aarName;
}

Future<AAR> handleFlutter() async {
  var processResult = await Process.run(
    "flutter",
    ["build", "apk"],
    workingDirectory: "..",
    runInShell: true,
  );

  print(processResult.stdout);

  var name = "flutter-release.aar";

  var file = File("../.android/Flutter/build/outputs/aar/flutter-release.aar");
  var target = file.copySync("${outputDir.path}/$name");

  return AAR()
    ..file = target
    ..aarName = name;
}

class AAR {
  String aarName;
  File file;

  String get noExtensionAarName => aarName.split(".").first;

  
Dart

大概解释下脚本的功能:

  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 文件都放在一起 (我的示例代码是放在一个仓库里的, 不过是同级目录)

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

tree -L 2
.
├── README.md
├── android-host
│   ├── android-host.iml
│   ├── app
│   ├── build
│   ├── build.gradle
│   ├── gradle
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── local.properties
│   └── settings.gradle
├── flutter-aar
│   ├── flutter-release.aar
│   └── shared_preferences-release.aar
└── flutter_module
    ├── README.md
    ├── build
    ├── flutter_module.iml
    ├── flutter_module_android.iml
    ├── lib
    ├── output
    ├── pubspec.lock
    ├── pubspec.yaml
    ├── shell
    ├── template
    └── test

Bash

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

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

flutter 组允许访问 flutter_module 和 flutter-aar

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

修改 android 主工程

build.gradle:


def aarDir = "${rootProject.projectDir.path}/../flutter-aar"

repositories {
    flatDir {
        dirs aarDir
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    def file = new File(aarDir)
    file.listFiles(new FilenameFilter() {
        @Override
        boolean accept(File dir, String name) {
            return name.endsWith("aar")
        }
    }).each { f ->
        def aar = f.name.split("\\.").first()
        println("f.name = ${f.name} , aar = $aar")
        api(name: f.name.split("\\.").first(), ext: 'aar')
    }
}

Groovy

这样的情况下这个目录就完成了对于所有 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 添加到已有工程的方案, 虽然字数多, 但是实际引入并不复杂

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

嗯,仓库在这里: gitee

以上