Native 工程整合Flutter 的兩種方式

國家一級外賣員發表於2019-10-20

Native 工程整合Flutter 的兩種方式

一、前言

1.1 - flutter 是什麼 ?

Flutter是Google用以幫助開發者在Ios和Android兩個平臺開發高質量原生應用的全新移動UI框架

優勢:

  • 熱過載。利用Android Studio直接一個ctrl+s就可以儲存並過載,模擬器立馬就可以看見效果。
  • 一切皆為 Widget 的理念。對於Flutter來說,手機應用裡的所有東西都是Widget,通過可組合的空間集合、豐富的動畫庫以及分層課擴充套件的架構實現了富有感染力的靈活介面設計。
  • 藉助可移植的GPU加速的渲染引擎以及高效能原生程式碼執行時以達到跨平臺裝置的高質量使用者體驗。利用Flutter構建的應用在執行效率上會和原生應用差不多。

具體介紹可參考:你好,Flutter

1.2 - 新建 Flutter 工程

如不做特殊說明,文中所操作的工程都是 Module 工程。

在開始接入之前,先在Android Studio下載Flutter、Dart兩個外掛,外掛安裝完成之後,新建一個Flutter工程。

Flutter工程有四種型別選擇,開發主要選擇 Flutter Application 和 Flutter Module。

Application 與 Module 工程差別

  • 前者作為一個獨立的app執行,後者的Flutter子模組可以作為其他工程的依賴。
  • 前者用資料夾android來儲存android程式碼,後者使用.android來儲存,且後者的.android資料夾是隱藏的。
  • 進入android資料夾內部,前者只有app一個工程,在新建的時候可以選擇是否支援kotlin或者swift,後者有兩個工程appFlutterFlutter作為子模組被app所依賴。

1.3 - 一些重要的 gradle 檔案

  • app 模組下的 build.gradle
android {
    .......
}

// 在編譯過程中產生的中間產物將會存放在該資料夾下
buildDir = new File(rootProject.projectDir, "../build/host")

dependencies {
    implementation project(':flutter')
    ......
}
複製程式碼

主工程 app 依賴了子模組 :flutter,但是我們在工程中並沒有使用以flutter命名的子模組。

  • Flutter 模組下的 build.gradle
def localProperties = new Properties()
def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, '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'
}
複製程式碼

在初次點開該 gradle 檔案時會在 GradleException 處標紅,不用解決,不影響正常編譯。

這邊主要是從local.properties獲取一些配置資料,如 flutter sdk 位置等等。

apply plugin: 'com.android.library'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 28

    defaultConfig {
        ......
    }
}

flutter {
    source '../..'
}

dependencies {
    ......
}
複製程式碼

在第二行通過apply from 引入了 flutter.gradle 檔案,其主要作用是為 Flutter模組引入 flutter 相關依賴,.so檔案等等。

注意:flutter 模組要求 compileSdkVersion >= 28,對於很多使用 kotlin 程式碼且工程 sdk 低於 28 的可能有毀滅性的打擊,會報 onCreate() override nothing等型別不匹配的錯誤,需要一一手動修改錯誤。

flutter 結構體主要表示flutter原始碼的位置,../..的意思就是Flutter資料夾下的程式碼就是原始碼。

  • .android/setting.gradle
include ':app'

rootProject.name = 'android_generated'
setBinding(new Binding([gradle: this]))
evaluate(new File(settingsDir, 'include_flutter.groovy'))
複製程式碼

開啟檔案後,Binding類也會報錯,此時不要為其 import ,不然會報錯,報紅不影響編譯。

這幾行程式碼的意思就是說,將 Flutter 模組引入到Android工程中。Flutter 模組並沒有顯示地使用 include ':flutter' ,那為什麼 Flutter 模組會以 :flutter 這樣的形式被依賴呢?帶著問題,我們看看include_flutter.groovy

  • .android/include_flutter.groovy
gradle.include ':flutter'
gradle.project(':flutter').projectDir 
           = new File(flutterProjectRoot, '.android/Flutter')
複製程式碼

可以看到,檔案內仍然是通過 include ':flutter' 語法引入到 Android 工程內,同時為其指定模組位置為 Flutter 資料夾

if (System.getProperty('build-plugins-as-aars') != 'true') {
    def plugins = new Properties()
    def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins')
    if (pluginsFile.exists()) {
        pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
    }

    plugins.each { name, path ->
        def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile()
        gradle.include ":$name"
        gradle.project(":$name").projectDir = pluginDirectory
    }
}
複製程式碼

這邊是呼叫 flutter build aar 指令(僅在 flutter module 工程下可用)編譯輸出 aar 檔案時會被呼叫的,其目的就是將 flutter 工程用到的第三方元件打到輸出到 aar 中方便 Native 工程引入。

  • flutterRoot/packages/flutter_tools/gradle/flutter.gradle

flutterRoot 是 flutter sdk 所在的位置。具體內容可以檢視 揭開Flutter工程編譯的面紗(Android篇)

二、接入

目前 Native 工程接入 Flutter 工程有兩種方式:

  1. 將上文提到的Flutter模組作為依賴引入到 Native
  2. 以 aar 形式引入到 Native 工程

2.1 - 以 module 方式整合

該方式整合是最簡單輕鬆的。native 工程名稱為 FlutterNativeProject,flutter 工程名稱為 flutter_module_project

2.1.1 - 步驟1

將 flutter module 工程整個拷入到 native 工程中。(本文只是從 git 管理的角度放在 native 工程下,當然也可以在其他位置,只需在配置檔案配置)

Native 工程整合Flutter 的兩種方式

2.1.2 - 步驟2 :修改 setting.gradle

include ':app'
rootProject.name='FlutterNativeProject'
rootProject.name = 'android_generated'
setBinding(new Binding([gradle: this]))
evaluate(new File(settingsDir,
'/flutter_module_project/.android/include_flutter.groovy'))
複製程式碼

注意修改 include_flutter.groovy 位置為實際工程中的地址,這邊引入 ':flutter',但 native 工程還沒有依賴它。

2.1.3 步驟3:修改 native 工程下 app 的 build.gradle

dependencies {
    implementation project(':flutter')
}
複製程式碼

試一把:在 MainActivity 裡面加入 Flutter 程式碼:

Native 工程整合Flutter 的兩種方式

報紅??????看一下報的錯誤吧。

Type mismatch.
Required: Lifecycle!
Found: androidx.lifecycle.Lifecycle
複製程式碼

看來是型別不匹配啊,androidx 是 support 包整合之後,用以解決 support 包混亂的問題,沒關係,換成 support(flutter 也支援 androidx,在新建工程的時候下方有個選項是否使用 androidX,28 版本是 support 庫最後支援的版本,後面都要使用 androidX)。同時,將 gradle.properties 設定為useAndroidX=falseenableJetifier=false

// 將 androidx 依賴改成這個
implementation "com.android.support:appcompat-v7:28.0.0"
複製程式碼
Native 工程整合Flutter 的兩種方式

再試一把:加入程式碼 setContentView(flutterView)

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        FlutterMain.startInitialization(this)

        val flutterView = Flutter.createView(this, lifecycle, "/main")
        setContentView(flutterView)
    }
}
複製程式碼

可以簡單理解為建立了一個 View,然後設定為佈局

@NonNull
public static FlutterView createView(@NonNull final Activity activity,
@NonNull final Lifecycle lifecycle, 
final String initialRoute)
複製程式碼

initialRoute:在 flutter 中設定的頁面路由,而在 flutter 工程預設生成的工程中,flutter_module_project/lib/main.dart沒有配置路由,給其配置一個路由:

Native 工程整合Flutter 的兩種方式

再試億把:編譯成功了,下載到手機中試一試 方式1:as 裡面的 run 方式2:開啟 terminal ,輸入 flutter run

作為未來的外賣員,該選哪個你心裡沒點數嗎?肯定第一種啊!

Native 工程整合Flutter 的兩種方式

Default interface methods are only supported starting with Android N (--min-api 24): void android.arch.lifecycle.DefaultLifecycleObserver.onCreate(android.arch.lifecycle.LifecycleOwner)
複製程式碼

在 native 工程的 build.gradle 裡面新增如下程式碼:

android {
    ......
    defaultConfig{
        ......
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
}
複製程式碼

到這裡就成了!我們是冠軍!

看一下成果(忽略這醜陋的一切):

Native 工程整合Flutter 的兩種方式

2.2 - 以 aar 形式引入

在開始騷操作之前,先解剖一下剛剛成功生成的 apk 檔案:

Native 工程整合Flutter 的兩種方式

libflutter.soflutter_assets是 flutter 執行必備的資源,前者是flutter 框架基礎,後者就是 lib 資料夾下的 dart 程式碼,這就是坑的開始。

2.2.1 - 步驟1:生成 aar

--> 進入 flutter module 工程

兩種方式:

  1. 進入 .android 資料夾,開啟 Terminal,輸入指令:
./gradlew assembleDebug
複製程式碼

編譯結束後,在.android/Flutter/build/outputs/aar下找到flutter-debug.aar

  1. 在 flutter 工程下(注意位置,一定要在 pubspec.yaml 同級目錄下)開啟 Terminal,輸入指令:
flutter build aar --debug  // 後面會解釋 debug 與 release 的區別
複製程式碼

--> 注意:我們在拉 flutter sdk 的時候,一般是在 github 上 clone,目前該指令只在 master 分支上有效。位置在flutter_module_project/build/host/outputs/repo/com/example/flutter_module_project/flutter_debug/1.0/flutter_debug-1.0.aar

兩種方式生成的 aar 檔案相同。

2.2.2 - 步驟2:在 native 工程中引用

為了引入 aar,需要在 native 外層的 build.gradle 中新增如下程式碼,不然會出現找不到 aar 檔案的問題:

allprojects {
    repositories {
        flatDir {
            dirs 'libs'
        }
    }
}
複製程式碼

將 flutter_debug-1.0.aar 拷貝到 app/libs資料夾下,在 app 下的 build.gradle 新增如下程式碼:

implementation(name : 'flutter_debug-1.0', ext : 'aar')
複製程式碼

回到 MainActivity 中,新增如下程式碼:

FlutterMain.startInitialization(this)
val flutterView = Flutter.createView(this, lifecycle, "/main")
setContentView(flutterView)
複製程式碼
Native 工程整合Flutter 的兩種方式

找不到依賴?

2.2.3 - 依賴找不到尋因

在1.1節中,以 module 方式引用依賴沒有出現任何問題,看看 native 工程的依賴:

Native 工程整合Flutter 的兩種方式

再看看以 module 方式引入的依賴:

Native 工程整合Flutter 的兩種方式
project :flutter
     +--- com.android.support:support-v13:27.1.1
     |    +--- com.android.support:support-annotations:27.1.1 -> 28.0.0
     |    \--- com.android.support:support-v4:27.1.1
     |         +--- com.android.support:support-compat:27.1.1 -> 28.0.0 (*)
     |         +--- com.android.support:support-media-compat:27.1.1
     |         |    +--- com.android.support:support-annotations:27.1.1 -> 28.0.0
     |         |    \--- com.android.support:support-compat:27.1.1 -> 28.0.0 (*)
     |         +--- com.android.support:support-core-utils:27.1.1 -> 28.0.0 (*)
     |         +--- com.android.support:support-core-ui:27.1.1 -> 28.0.0 (*)
     |         \--- com.android.support:support-fragment:27.1.1 -> 28.0.0 (*)
     +--- com.android.support:support-annotations:27.1.1 -> 28.0.0
     +--- io.flutter:flutter_embedding_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852
     |    +--- android.arch.lifecycle:common:1.1.1 (*)
     |    +--- android.arch.lifecycle:common-java8:1.1.1
     |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
     |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
     |    +--- android.arch.lifecycle:runtime:1.1.1 (*)
     |    +--- com.android.support:support-fragment:28.0.0 (*)
     |    \--- com.android.support:support-annotations:28.0.0
     +--- io.flutter:armeabi_v7a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852
     \--- io.flutter:arm64_v8a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852
複製程式碼

原來我們需要的都是在flutter_embedding_debug這個依賴下面,但是以 aar 形式引入缺少該依賴。

上文提到 flutter.gradle 的工程是為 flutter 工程新增必要的依賴讀過揭開Flutter工程編譯的面紗(Android篇)發現跟我本地的 flutter.gradle 有不同之處,文章中說 flutter.jar 被直接作為依賴引入,而我們工程中引用的是flutter_embedding_debug.jar這個依賴,在 flutter.gradle 尋找關鍵字。

Native 工程整合Flutter 的兩種方式
原來,flutter_embedding_debug 是從遠端倉庫"download.flutter.io"下載下來的,那我們就在 native 工程中引用吧!

allprojects {
    repositories {
        ......
        maven { url "http://download.flutter.io" }
        flatDir {
            dirs 'libs'
        }
    }
}
複製程式碼

app 下的 build.gradle 新增如下依賴

dependencies {
    implementation "io.flutter:flutter_embedding_release:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
}
複製程式碼

--> 注意:後面的長串字母是引擎號,要跟你的 flutter sdk 相匹配,不能直接用文章中的。

果然沒讓我失望,報錯了:

java.lang.UnsatisfiedLinkError: 
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.flutternativeproject-1/base.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]] 
couldn't find "libflutter.so"
複製程式碼

沒找到 "libflutter.so",說明依賴還沒有全,我們繼續找 libflutter.so 在哪個依賴下面:

Native 工程整合Flutter 的兩種方式

Native 工程整合Flutter 的兩種方式

libflutter.so 放在這個依賴裡面,繼續在 "flutter.gradle" 裡面尋找答案:

Native 工程整合Flutter 的兩種方式

flutter 支援這四種 cpu 架構,並且將相應架構的 libflutter.so 加入到工程依賴中,這樣工程不管依賴哪個,都會存在 libflutter.so。所以我們也需要讓 native 工程擁有這些依賴。

Native 工程整合Flutter 的兩種方式
我們把四個都加入到 native 工程中,完整的app 下 build.gradle 如下:

dependencies {
    implementation "io.flutter:flutter_embedding_release:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    implementation "io.flutter:arm64_v8a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    implementation "io.flutter:armeabi_v7a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    implementation "io.flutter:x86_64_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    implementation "io.flutter:x86_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    implementation(name : 'flutter_debug-1.0', ext : 'aar')
複製程式碼

Native 工程整合Flutter 的兩種方式

我可太牛了,成功黑屏,"libflutter.so"和依賴庫全都有了,而且 flutter.gradle 裡面也是這樣的,why?總覺得哪裡有不對勁的地方。回顧我之前接百度地圖 sdk 的時候,人家也沒有讓我在 app 的 libs 裡面加這個so,那個so的,不都是直接 implementment 相應的aar就好了嘛,aar 裡面不就應該有 so 嗎?想到這裡,扒開 flutter_debug-1.0.aar 的外衣:

Native 工程整合Flutter 的兩種方式

jni 檔案都沒有!為啥 aar 打包的時候,依賴都沒有被打到 aar 裡面,我承認在這時候我產生了自我懷疑,“谷歌爸爸肯定不可能坑我們的”,確定自己操作沒有問題之後,搜尋答案:

flutter build aar 指令並不會將依賴的 module 或者 libary 打入到 aar 中,需要搭配 fat-aar 。

github 中原版的 fat-aar 不支援 gradle 3.0+,可以使用 kezong\fat-aar-android。按照教程配置之後的 Flutter 下 build.gradle:

// 是否將遠端依賴也打包進去
configurations.embed.transitive = false

dependencies {
    embed "io.flutter:flutter_embedding_release:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:arm64_v8a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:armeabi_v7a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:x86_64_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:x86_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
}
複製程式碼

再次 flutter build aar --debug,將 native 工程 app 中的 flutter 依賴全部移除,僅依賴 flutter aar 即可。

打出的 aar 中在 libs 檔案下確實包含了相應的 library,在 native 工程中再打包試一下吧!

如果你試過了就會發現,仍然出不來,黑屏也不報錯!

對比flutter run 生成的 apk 與 native 工程生成的 apk:

Native 工程整合Flutter 的兩種方式

在lib下的cpu架構不一致,缺少x86相關的,但是 x86 是用於平板的,跟手機沒關係呀。

再仔細檢視依賴,發現

embed "io.flutter:flutter_embedding_release:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
複製程式碼

我的指令是 flutter build aar --debug,應該依賴的是 debug,所以改成 debug 試一試

embed "io.flutter:flutter_embedding_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
複製程式碼

再打成 aar 整合到 native 裡面,發現正常執行了!Ok,到這裡 aar 編譯成功!我們又是冠軍!

三、編譯 aar 快速整合

OK,上面講的是踩坑排坑的過程,這一章將介紹如何快速整合(Flutter Module 工程)。

3.1 - 步驟1:新增fat-aar外掛

--> .android/build.gradle

buildscript {
    dependencies {
        ...
        classpath 'com.kezong:fat-aar:1.2.7'
    }
}
複製程式碼

--> .android/Flutter/build.gradle

// 是否將遠端依賴也打包進去
configurations.embed.transitive = false

// 新增 flutter 依賴
dependencies {
    embed "io.flutter:flutter_embedding_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:arm64_v8a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:armeabi_v7a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:x86_64_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
    embed "io.flutter:x86_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
}
複製程式碼

--> 不要忘記 Maven 庫

allprojects {
    repositories {
        ......
        maven { url "http://download.flutter.io" }
        flatDir {
            dirs 'libs'
        }
    }
}
複製程式碼

3.2 - 步驟2:打包指令

切到pubspec.yaml同級目錄,開啟 terminal,寫入指令flutter build aar --debug,aar 生成於 你的flutter工程目錄/build/host/outputs/repo/你的包名/flutter_debug/1.0/

3.3 - 步驟3:native 工程引入即可

四、總結 & 疑問

Question1:flutter build aar 生成 release 和 debug 的區別?

答: 資料:深入理解flutter的編譯原理與優化

Debug模式:對應了Dart的JIT模式,又稱檢查模式或者慢速模式。支援裝置,模擬器(iOS/Android),此模式下開啟了斷言,包括所有的除錯資訊,服務擴充套件和Observatory等除錯輔助。此模式為快速開發和執行做了優化,但並未對執行速度,包大小和部署做優化。Debug模式下,編譯使用JIT技術,支援廣受歡迎的亞秒級有狀態的hot reload。

Release模式:對應了Dart的AOT模式,此模式目標即為部署到終端使用者。只支援真機,不包括模擬器。關閉了所有斷言,儘可能多地去掉了除錯資訊,關閉了所有除錯工具。為快速啟動,快速執行,包大小做了優化。禁止了所有除錯輔助手段,服務擴充套件。

總言之:debug 沒有優化,方便開發;release 很多優化,用於最終版本。同時在 flutter_embedding_debug依賴包裡面有個配置相關類 BuildConfig

public final class BuildConfig {
    public static final boolean DEBUG = true;
    public static final boolean PROFILE = false;
    public static final boolean RELEASE = false;
    public static final boolean JIT_RELEASE = false;

    private BuildConfig() {
    }
}
複製程式碼

所以當依賴包不匹配的時候,真機能執行,不報錯,但是黑屏,應該就是 libflutter.so 沒有正確初始化的原因,所以在工程中一定要保證依賴庫的正確。從整合過程來看,aar 方式容易出錯,還是建議以 module 依賴的形式進行混合開發。

Question2:flutter.gradle 裡面的 flutter.jar 是什麼?

答:

if (useLocalEngine()) {
    String engineOutPath = project.property('localEngineOut')
    File engineOut = project.file(engineOutPath)
    if (!engineOut.isDirectory()) {
        throw new GradleException('localEngineOut must point to a local engine build')
    }
    Path baseEnginePath = Paths.get(engineOut.absolutePath)
    flutterJar = baseEnginePath.resolve("flutter.jar").toFile()
    if (!flutterJar.isFile()) {
        throw new GradleException("Local engine jar not found: $flutterJar")
    }
    localEngine = engineOut.name
    localEngineSrcPath = engineOut.parentFile.parent
    // The local engine is built for one of the build type.
    // However, we use the same engine for each of the build types.
    project.android.buildTypes.each {
        addApiDependencies(project, it.name, project.files {
            flutterJar
        })
    }
} else {
    project.android.buildTypes.each this.&addFlutterDependencies
    project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies
}
複製程式碼

flutter 指令支援本地引擎的編譯,但是引擎需要提前準備好,大概編譯就需要一個半小時,我們就用預設的引擎就好了。如果使用本地引擎,就會 sdk 目錄下的 engine 裡面使用 flutter.jar 作為依賴引入,它內部結構如下:

Native 工程整合Flutter 的兩種方式

內容結構跟我們通過 embed 引入的遠端依賴是相似的,但是在 engine 目錄下有很多 cpu 架構的 flutter.jar 檔案,根據自己的需要引入。既然結構相似,那是否可以通過依賴 flutter.jar 的形式打 aar 引入到 native 工程呢?經過實測,答案是YES,我使用的是 android-arm 檔案下的 flutter.jar。

Question3:為什麼要整合 Flutter ?

請從下列選項中選出符合條件的選項:(D)

A:新技術

B:一份程式碼兩端執行

C:google

D:“殺”程式猿省錢

相關文章