阅读 8001

Flutter混合工程开发探究

作者:腾讯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混合工程

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变的更好!