Android 项目接入Flutter

一、官方方案

创建Android项目

如果你已经有Android项目,可以直接使用。这里我们先创建一个空的android项目来模拟已有的项目,取名叫TestFlutter。

创建Flutter模块

在Android工程目录同级目录下执行命令

flutter create -t module flutter_module

上面的命令会创建一个flutter的项目模块,在flutter文件夹中有一个.android的隐藏文件夹,里面包裹了一个安卓库的工程模块。

可以尝试用Gradle编译这个库,但这不是必须的步骤:

$ cd .android/
$ ./gradlew flutter:assembleDebug

编译后会在.android/Flutter/build/outputs/aar/路径下产生flutter-debug.aar的文件。

将Flutter模块作为依赖添加到主项目

打开你的Android工程的setting.gradle文件,添加如下代码:

include ':app'                                   
setBinding(new Binding([gradle: this]))                                 
evaluate(new File(                                                      
  settingsDir.parentFile,                                               
  'flutter_module/.android/include_flutter.groovy'                          
))                                                                      

这几行代码的意思就是将你刚才创建的那个module作为android模块引入到Android工程中。

点击同步完成后,到你app目录的build.gradle文件把依赖加上:

dependencies {
  implementation project(':flutter')
}

再次同步完成就已经将Flutter添加到了你的项目了。接下来就可以开始混合开发了。

遇到问题总结

1、比如,我们不在Android工程的同级目录去flutter create -t module my_flutter会怎么样,我尝试了,只需要对路径加上你工程目录名即可,这么写

setBinding(new Binding([gradle: this]))                                
evaluate(new File(                                                      
        settingsDir.parentFile,                                           
        '你工程目录名/flutter_module/.android/include_flutter.groovy'                        
))

2、release 混淆问题

-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }

二、闲鱼方案

通过Android studio 新建Flutter Application项目

在命令行输入命令flutter build apk
会编译生成apk文件,位于build/app/outputs/apk/release/文件夹下。

这个apk里的产物实际上是在Android的app/build.gradle构建代码里引入了Flutter的构建代码。

apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

通过阅读flutter构建源码我们发现在构建apk文件的时候,会将需要的文件构建到apk中。

1.assets文件夹

assets文件夹下面有flutter_assets文件夹、flutter_shared文件夹、isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data、vm_snapshot_instr文件。

flutter_assets里是flutter工程产生的assets文件
flutter_shared里是封装在flutter.jar里面的处理字符编码的ICU库
isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data、vm_snapshot_instr为特定平台的数据和指令

2.lib文件夹

lib文件夹下是特定平台(arm或者x86)的so文件。

flutter在Android平台下会默认生成arm-v7架构的的so库,flutter.gradle源码中会根据target-platform属性判断平台动态生成对应的so,官方注释目前flutter只支持在debug模式下生成x86的so。

提取aar

上面通过编译命令得到了apk,那想要打包成aar,理论上只要把app/build.gradle中的apply plugin: 'com.android.application'修改为apply plugin: 'com.android.library',同时删除applicationId "com.shanbay.flutterapp"再次执行flutter build apk命令,便可以得到app-release.aar文件。

或者进入android文件目录下,执行命令 ./gradlew assembleRelease 也可以编译得到app-release.aar

集成到现有项目

我们将得到的aar文件集成到现有的Android工程中使用,但是打开flutter页面却闪退了,同时flutter报出了error,错误是说aar里面缺少icudtl.dat文件。

解压缩aar查看文件结构,可以发现其中的问题。

image.png

在aar文件夹下的assets里面缺少了flutter_shared文件夹,icudtl.dat文件正是在该文件夹里面,也就是说flutter.gradle在编译流程中并没有将icudtl.dat文件打进aar包里面,这一点从flutter库的issue里面得到了证实,我们的办法是将apk里面得到的flutter_shared文件夹手动copy到flutter工程中,再次编译aar,这样就可以得到有icudtl.dat的aar文件。再次集成到Android项目中便可以成功运行,不会产生错误。

总结

这个方案需要两个步骤,第一步是先编译成apk取得icudtl.dat文件放入到工程中,第二步修改apply plugin: 'com.android.library'再次编译取得aar。

如果Flutter项目引用了path_provider、shared_preferences 需要将这两个jar包导入aar中不然会提示找不到这两个jar包的PathProviderPlugin、SharedPreferencesPlugin

image.png

在app的build.gradle 里面添加代码

implementation fileTree(dir: 'libs', include: ['*.jar'])

为了方便打aar包和flutter项目运行,我们可以设置一个变量来控制

在android目录下的gradle.properties里面添加BUILD_MODE=aar,src目录下新建一个文件夹里面放置AndroidManifest.xml

image.png

在app的build.gradle中添加以下代码

image.png
image.png
image.png
image.png

之后需要打aar包的时候,只需要在gradle.properties中修改BUILD_MODE为aar就可以用来打aar包,修改为flutter就是正常的flutter编译模式

build.gradle 相关代码

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}
def BUILD_MODE = localProperties.getProperty('BUILD_MODE')
def isRunAsFlutter = "flutter".equals(BUILD_MODE)

if (isRunAsFlutter) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 28

    lintOptions {
        disable 'InvalidPackage'
    }

    defaultConfig {
        if (isRunAsFlutter) {
            applicationId "com.test.flutter"
        }
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            signingConfig signingConfigs.debug
        }
    }

    sourceSets {
        main {
            def srcFile = isRunAsFlutter ? 'src/main/AndroidManifest.xml' : 'src/maven/AndroidManifest.xml'
            manifest.srcFile srcFile
            java {
                srcDir 'src/main/java'

            }
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
            jniLibs.srcDirs = ['src/main/jniLibs']
        }
    }

    configurations.all {
        resolutionStrategy {
            cacheChangingModulesFor 0, 'seconds'
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    if (!isRunAsFlutter) {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    }
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:support-v13:28.0.0'
    implementation 'com.android.support:support-annotations:28.0.0'
}

参考:

Flutter在混合项目中的构建和集成
Flutter混合开发和动态更新的探索历程 Android版

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容