Flutter混合工程開發探究

NOW終端技術團隊發表於2018-07-23

作者:騰訊NOW直播 -koudleren(任曉帥)

團隊介紹

騰訊NOW終端技術團隊在Flutter推出後就一直在關注Flutter的發展,並且在2018年4月份將Flutter引入到NOW直播APP中,在將Flutter運用在業務中的同事,也一直在研究並完善Flutter的技術,希望將自己團隊的技術和經驗分享給其他團隊。

前言

Flutter作為一個新的UI開發框架,因為其創新的理念,已經吸引了越來越多的人蔘與其中,在實際的專案開發中,我們更希望將Flutter引入到我們的APP中,而不是用Flutter重新開發一款全新的APP。所以作為開發者,我們更關心的是,如何使用Flutter進行混合工程的開發?值得關注的是,將Flutter整合到現有的工程中的方法, 隨著Flutter框架的升級也在不斷變換中。而本文將介紹NOW直播在混合開發中採用的方法,首先帶領大家從Flutter外掛和編譯指令碼入手,研究Flutter的構建工程目錄結構,最後自己實現Flutter混合工程開發的外掛,給其他團隊以參考。

從建立Flutter工程說起

按照官網的提示,要進行Flutter的開發,要如下幾步:

1、下載Flutter SDK

2、配置環境變數

3、在Android Studio上安裝Flutter和Dart外掛

4、File > New Flutter Project > Flutter application > Enter a project name > Finish

之後在Android Studio 的toolbar上就可以看到如下的圖示:

Main IntelliJ toolbar

1.Flutter 外掛

我們點選Android Studio 上的New選單,建立Flutter工程,然後點選Android Studio toolbar上的Run執行Flutter程式,不禁思考,為什麼Flutter工程要這樣建立,在點選Run之後發生了什麼,Hot Reload是怎麼實現的,為了一探究竟,我們可以從Flutter 的Android Studio外掛原始碼入手。

在GitHub上,有Flutter Android Studio外掛的原始碼,地址為:

https://github.com/flutter/flutter-intellij

通過閱讀外掛的原始碼,可以發現外掛實現不同功能的程式碼邏輯是怎樣的,我們可以根據外掛的配置資訊查詢具體的外掛動作及其實現類,

外掛配置資訊所在的檔案為:resources/META-INF/plugin.xml

下面我們將分別找到New Flutter Project 和 Run Flutter Project的配置資訊,並進行分析。

1.1 New Flutter Project

<!-- Define the 'New Flutter Project' menu item -->
    <action id="flutter.NewProject" class="io.flutter.actions.FlutterNewProjectAction"
            text="New Flutter Project..."
            description="Create a new Flutter project">
      <add-to-group group-id="NewProjectOrModuleGroup" anchor="after" relative-to-action="NewProject"/>
    </action>

複製程式碼

在註釋裡可以看到,這個配置資訊就是New Flutter Project的,action 的 id 是flutter.NewProject,對應的類檔案是io.flutter.actions.FlutterNewProjectAction,

於是找到這個類,分析裡面實現的類,可以發現如下的關係:

FlutterNewProjectAction(新建工程的入口) > FlutterProjectModel(填寫工程資訊的視窗) > FlutterProjectCreator (工程建立準備及驗證) > FlutterSmallIDEProjectGenerator(獲取Flutter SDK資訊)> FlutterSdk (呼叫了Flutter SDK的命令列程式碼)

在Flutter工程建立的過程中,最核心的程式碼其實是這一行:

return new FlutterCommand(this, appDir.getParent(), FlutterCommand.Type.CREATE, vargs)
複製程式碼

這一句其實是執行了Flutter SDK的一句命令列:

$ flutter create --template=app --org=now.tencent.com flutter_app
複製程式碼

使用Flutter的命令列建立了工程。

1.2 Run Flutter Project

<!-- main toolbar run actions -->
    <action id="Flutter.Toolbar.ReloadAction" class="io.flutter.actions.ReloadFlutterAppRetarget"
            description="Reload"
            icon="FlutterIcons.HotReload">
      <add-to-group group-id="ToolbarRunGroup" anchor="after" relative-to-action="RunnerActions"/>
      <keyboard-shortcut keymap="$default" first-keystroke="ctrl BACK_SLASH"/>
    </action>

<!-- run menu actions -->
    <group id="Flutter.MenuActions.Run">
      <separator/>
      <reference ref="Flutter.Toolbar.ReloadAction"/>
      <action id="Flutter.Toolbar.RestartAction" class="io.flutter.actions.RestartFlutterAppRetarget"
              description="Restart"
              icon="FlutterIcons.HotRestart">
        <keyboard-shortcut keymap="$default" first-keystroke="ctrl shift BACK_SLASH"/>
        <keyboard-shortcut keymap="$default" first-keystroke="ctrl shift S"/>
      </action>
      <action id="Flutter.Menu.RunProfileAction" class="io.flutter.actions.RunProfileFlutterApp"
              description="Flutter Run Profile Mode"
              icon="AllIcons.Actions.Execute">
      </action>
      <action id="Flutter.Menu.RunReleaseAction" class="io.flutter.actions.RunReleaseFlutterApp"
              description="Flutter Run Release Mode"
              icon="AllIcons.Actions.Execute">
      </action>
      <separator/>
      <add-to-group group-id="RunMenu" anchor="after" relative-to-action="Stop"/>
    </group>
複製程式碼

同樣的原理,可以發現核心的程式碼如下:

[SdkRunConfig.java]
return fields.createFlutterSdkRunCommand(project, mode, FlutterLaunchMode.fromEnv(env), device);
複製程式碼
[SdkFields.java]
final FlutterCommand command = flutterSdk.flutterRun(root, main.getFile(), device, runMode, flutterLaunchMode, args);
    return command.createGeneralCommandLine(project);
複製程式碼

其實是執行了Flutter SDK的一句命令列:

$ flutter run ...
複製程式碼

將Flutter程式Run起來,並實現了Hot Reload。

2.Flutter編譯指令碼

如果說Flutter的外掛,是將Flutter和Android Studio(IDE)連線在一起,那麼Flutter的gradle編譯指令碼就是將Flutter和Android連線在一起:在Android工程中新增Flutter的依賴,並採用不同的編譯策略,生成Flutter的構建產物,最後將Flutter產物打包到Android的APK中。

Flutter編輯指令碼所在的位置是Flutter SDK資料夾下的packages/flutter_tools/gradle/flutter.gralde

接下來我們將分析Flutter編譯指令碼的程式碼,研究Flutter編譯指令碼的作用。主要做的內容包括:

2.1 獲取Flutter 環境變數

包括:

1、Flutter SDK的路徑

2、獲取flutter.bat 的路徑

3、Flutter jar 包的路徑

4、編譯模式

等等,部分程式碼如下:

String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)

2.2 新增Flutter的依賴,包括jar包和so包

根據buildMode的不同,依賴不同的配置,部分程式碼如下:

/**
 * Adds suitable flutter.jar api dependencies to the specified buildType.
 *
 * Note: The BuildType DSL type is not public, and is therefore omitted from the signature.
 */
private void addFlutterJarApiDependency(Project project, buildType) {
    project.dependencies {
        String configuration;
        if (project.getConfigurations().findByName("api")) {
            configuration = buildType.name + "Api";
        } else {
            configuration = buildType.name + "Compile";
        }
        add(configuration, project.files {
            String buildMode = buildModeFor(buildType)
            if (buildMode == "debug") {
                [flutterX86Jar, debugFlutterJar]
            } else if (buildMode == "profile") {
                profileFlutterJar
            } else {
                releaseFlutterJar
            }
        })
    }
}
複製程式碼

2.3執行Flutter的SDK對Dart程式碼構建產物

​ 我們知道Flutter是用Dart程式碼編寫的,為了讓Dart程式碼可以執行在Flutter engine上,需要對Flutter程式碼進行編譯,這裡有兩種編譯方式:

1、JIT編譯

​ JIT(Just In Time),即時編譯 ,Flutter在debug模式下使用此種編譯方式,可以實現Hot Reload

2、AOT編譯

​ AOT(Ahead Of Time),靜態提前編,編譯成本地機器碼 ,Flutter在Release模式下使用此種編譯方式,擁有更好的效能 。

FlutterTask flutterTask = project.tasks.create(name: "flutterBuild${variant.name.capitalize()}", type: FlutterTask) {
                flutterRoot this.flutterRoot
                flutterExecutable this.flutterExecutable
                buildMode flutterBuildMode
                localEngine this.localEngine
                localEngineSrcPath this.localEngineSrcPath
                targetPath target
                verbose verboseValue
                previewDart2 previewDart2Value
                fileSystemRoots fileSystemRootsValue
                fileSystemScheme fileSystemSchemeValue
                trackWidgetCreation trackWidgetCreationValue
                buildSnapshot buildSnapshotValue
                buildSharedLibrary buildSharedLibraryValue
                targetPlatform targetPlatformValue
                sourceDir project.file(project.flutter.source)
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
                extraFrontEndOptions extraFrontEndOptionsValue
                extraGenSnapshotOptions extraGenSnapshotOptionsValue
            }
複製程式碼

2.4將構建的產物copy到Android assets目錄下

​ 在使用不同編譯器編譯的時候生成的產物也不一樣,具體檢視下面2.5的圖。


            Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
                dependsOn flutterTask
                dependsOn variant.mergeAssets
                dependsOn "clean${variant.mergeAssets.name.capitalize()}"
                into variant.mergeAssets.outputDir
                with flutterTask.assets
            }
複製程式碼

2.5Flutter APK目錄結構

​ 講過上面的介紹,再來看Flutter APK的目錄結構,就很容易理解,下圖分別是Flutter Debug APK和Release APK目錄結構的對比:

Flutter混合工程開發探究

建立Flutter混合工程

NOW直播從Flutter的0.3.1版本開始接入,到現在Flutter釋出到0.5.7,見證了Flutter建立混合工程的方法的變化,方法雖然在變,但是原理不變,經過上面介紹的Flutter外掛和編譯指令碼,我們很容易理解Flutter建立混合工程的原理:就是經dart程式碼經過Flutter SDK編譯,然後把生成的產物打包到制定的APK路徑下。現在將告訴你如何建立Flutter的混合工程。

Flutter 最新版本0.5.7建立混合工程的方法如下:

1、在已有工程的同級目錄裡,執行:

$ flutter create -t module my_flutter
複製程式碼

2、在已有工程的settings.gradle裡新增Flutter 的model,如下:

// MyApp/settings.gradle
include ':app'          // assumed existing content
setBinding(new Binding([gradle: this]))    // new
evaluate(new File(                        // new
  settingsDir.parentFile,                 // new
  'my_flutter/.android/include_flutter.groovy'  // new
))         // new
複製程式碼

3、在已有工程的build.gradle裡新增Flutter model的依賴,如下:

// MyApp/app/build.gradle
:dependencies {  
	implementation project(':flutter')  
	:
}
複製程式碼

經過上面的步驟我們就建立了Flutter的混合工程,將Flutter工程Run起來並使用Hot Reload 的方法有兩種:

1、使用flutter run

$ cd MyApp
$ ./gradlew app:assembleDebug
$ cd ../xyz
$ flutter run --use-application-binary \
    ../MyApp/app/build/outputs/apk/debug/app-debug.apk
複製程式碼

2、使用flutter attach

cd <path to your Flutter module>
flutter attach
複製程式碼

在混合工程裡使用Flutter,發現純Flutter 工程裡在Android Studio 的toobar上顯示的icon不見了,想使用Flutter,只能使用命令列來執行,所以其實我們可以在Flutter外掛上擴充套件混合工程建立的命令及Run 和 HotReload的命令。

自己實現Flutter混合工程的外掛

在NOW直播裡為了方便的進行Flutter混合工程的開發,自己實現了一個外掛,示例如下:

 
    <action id="New-Flutter-Model" class="now.tencent.FlutterNewModel"
            text="New Flutter Model"
            description="Create a new Flutter model">
      <add-to-group group-id="NewProjectOrModuleGroup" anchor="after" relative-to-action="NewProject"/>
    </action>
    
   <action id="Hot-reload" class="now.tencent.now.HotReload" text="HotReload" description="HotReload when has started">
      <add-to-group group-id="ToolbarRunGroup" anchor="after" relative-to-action="RunnerActions"/>
      <keyboard-shortcut keymap="$default" first-keystroke="shift ctrl alt 0"/>
    </action>

    <action id="Start-Hot" class="now.tencent.now.StartHotReload" text="StartHotReload" description="StartHotReload">
      <add-to-group group-id="ToolbarRunGroup" anchor="after" relative-to-action="RunnerActions"/>
      <keyboard-shortcut keymap="$default" first-keystroke="shift ctrl alt 9"/>
    </action>
複製程式碼

這個外掛,NOW直播將在之後開源,歡迎關注。

總結

經過上面的步驟,NOW直播團隊已經解決了Flutter混合工程開發的問題,通過實現自定義的外掛,解決了實際開發過程中的問題,也在不斷完善Flutter的生態,讓我們期待Flutter變的更好!

相關文章