In the previous article we discussed how the Flutter App can be integrated into the iOS App. In this article we will discuss how the Flutter App can be integrated into the Android App.

Gradle

Let’s take a quick look at the code structure of the Android project:

As an automated build tool for the Android project, let’s take a look at what Gradle has done to build the Flutter APP.

settings.gradle

Settings. gradle is used to configure all dependent modules in Android Project.

settings.gradle
// 1 include ':app' // 2 def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath ! = null, "flutter.sdk not set in local.properties" // 3 apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"Copy the code
  1. Let’s start with the App Module
  2. readlocal.propertiesIn this fileflutter.sdkThe value of the property is assigned toflutterSdkPathThis variable
  • In addition to the Android SDK path, local. Properties also defines Flutter related values. For example, the Flutter SDK path, Flutter build mode, Flutter version number, etc.
local.properties
sdk.dir=/Users/*/Library/Android/sdk flutter.sdk=/Users/*/Documents/flutter flutter.buildMode=debug Flutter. VersionName = 1.0.0 flutter. VersionCode = 1Copy the code
  1. The introduction of"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"The script in the

Let’s look at the app_plugin_loader.gradle script:

app_plugin_loader.gradle
Import groovy. Json. JsonSlurper def flutterProjectRoot = rootProject. ProjectDir. ParentFile find configuration file / / 1 def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies') if (! Def object = new JsonSlurper().parseText(pluginsfile.text) assert object instanceof Map assert object.plugins instanceof Map assert object.plugins.android instanceof List object.plugins.android.each { androidPlugin -> assert androidPlugin.name instanceof String assert androidPlugin.path instanceof String def pluginDirectory = new File(androidPlugin.path, 'android') assert pluginDirectory.exists() include ":${androidPlugin.name}" project(":${androidPlugin.name}").projectDir  = pluginDirectory }Copy the code
  • readAndroidFile in the same directory.flutter-plugins-dependenciesfile
  • Read the Android array under the plugins field in that file and configure dependencies on each element of the array.

Hint: Familiar? That’s right, the iOS Pod script reads the iOS field values in the plugins field of this file.

{ "plugins":{ "android":[ ... { "name":"sqflite", "Path" : "/ Users / * / Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/", "dependencies" : []}...]. , "ios":[...] ,}}Copy the code

To summarize, settings.gradle produces something like this:

include ':app' include ':fijkplayer' project(":fijkplayer").projectDir = '/ Users / * / Documents/flutter/.pub-cache/hosted/pub.dartlang.org/fijkplayer-0.8.7/android' include ': shared_preferences' project(":shared_preferences").projectDir = '/ Users / * / Documents/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.5.12+4/android' include ': sqflite' Project (" : sqflite "). ProjectDir = '/ Users / * / Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/android'  include ':url_launcher' project(":url_launcher").projectDir = '/ Users / * / Documents/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-5.7.10/android'Copy the code

Summary: Settings. gradle completes the configuration of all dependent modules.

Project / build.gradle

Let’s look at some of the Settings in Project/build.gradle:

// 1. rootProject.buildDir = '.. /build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } // 2. subprojects { project.evaluationDependsOn(':app') }Copy the code
  1. Set the output path of project and sub-project in the build folder of the same level as android.

Check it out by going inside:

As you may have noticed, iOS builds are also in this directory.

  1. throughevaluationDependsOnDefines all the othersmouduleThe configuration depends onappthismoudule. Everything elsemouduleConfiguration has to waitappthismouduleAfter the configuration is complete, configure the.

Summary: Project/build.gradle configures the build output path of each moudule and dependencies between moudule.

app / build.gradle

Let’s take a look at app/build.gradle:

// 1. 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'} // 2. "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" // 3. flutter { source '.. /.. '}Copy the code

Code explanation:

  1. The first section reads the version number and version name from the local.properties file and sets it to the Android App version number and version name.
  2. According to the fromlocal.propertiesRead from the file$flutterRootPath to the import$flutterRoot/packages/flutter_tools/gradle/flutter.gradleScript run;
  3. toflutterThe extendedsourceProperty is configured to. /...

flutter.gradle

The purpose of flutter. Gradle is to perform some flutter related tasks in the build process of the Android host App.

Flutter. Gradle has two important classes: FlutterPlugin and FlutterTask.

FlutterPlugin

FlutterPlugin implements the Plugin interface as a Gradle Plugin, so its entry method is the apply() method:

FlutterPlugin/apply
void apply(Project project) { ... // 1. project.extensions.create("flutter", FlutterExtension) // 2 project.afterEvaluate this.&addFlutterTasks // 3 if (shouldSplitPerAbi()) { project.android { splits { abi { enable true reset() universalApk false } } } } getTargetPlatforms().each { targetArch -> String abiValue = PLATFORM_ARCH_MAP[targetArch] project.android { if (shouldSplitPerAbi()) { splits { abi { include abiValue } } } } } // 4 String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT) if (flutterRootPath == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.") } flutterRoot = project.file(flutterRootPath) if (! flutterRoot.isDirectory()) { throw new GradleException("flutter.sdk must point to the Flutter SDK directory") } engineVersion = useLocalEngine() ? "+" // Match any version since there's only one. : "1.0.0 -" + Paths. The get (flutterRoot absolutePath, "bin", "internal", "engine.version").toFile().text.trim() String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter" flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile(); // 5 project.android.buildTypes { profile { initWith debug if (it.hasProperty("matchingFallbacks")) { matchingFallbacks = ["debug", "release"] } } } // 6 if (shouldShrinkResources(project)) { String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "flutter_proguard_rules.pro") project.android.buildTypes { release { shrinkResources isBuiltAsApp(project) proguardFiles  project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro" } } } // 7 if (useLocalEngine()) { String engineOutPath = project.property('local-engine-out') File  engineOut = project.file(engineOutPath) if (! engineOut.isDirectory()) { throw new GradleException('local-engine-out must point to a local engine build') } localEngine = engineOut.name localEngineSrcPath = engineOut.parentFile.parent } // 8 project.android.buildTypes.each this.&addFlutterDependencies project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies }Copy the code

Code explanation:

  1. To create aFlutterExtensionExtension, this extension has two properties,sourceFlutter APPThe path of the project,targetFlutter APPExecution entry, do not set the defaultlib/main.dart;
  2. app moduleThe rest of theTaskExecute after completionaddFlutterTasksMethods;
  3. Decide whether to enable ABI subcontracting;
  4. Get some system environment variables;
    • flutterRootPath — /Users/*/Documents/flutter
    • flutterRoot — /Users/*/Documents/flutter
    • EngineVersion c956a31c0a3d350827aee6c56bb63337c5b4e6e 1.0.0-2
    • flutterExecutable — flutter(mac), flutter.bat(windows)
  5. By default, there are two debug and Release modes. In this case, a profile build mode was added based on the Debug mode, so now there are three debug, Release and profile build modes.
  6. Enable shrinkResources.
  7. Whether to set up a local Maven repository;
  8. Add to each build patternFlutterRely onaddFlutterDependenciesThe call.

Let’s look at the implementation in addFlutterDependencies:

FlutterPlugin/addFlutterDependencies
void addFlutterDependencies(buildType) { ... // 1. String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ? : DEFAULT_MAVEN_HOST String repository = useLocalEngine() ? project.property('local-engine-repo') : "$hostedRepository/download.flutter.io" project.rootProject.allprojects { repositories { maven { url repository } } } //  2 addApiDependencies(project, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion") print("io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion\n"); // 3 List<String> platforms = getTargetPlatforms().collect() if (flutterBuildMode == "debug" && ! useLocalEngine()) { platforms.add("android-x86") platforms.add("android-x64") } platforms.each { platform -> String arch  = PLATFORM_ARCH_MAP[platform].replace("-", "_") addApiDependencies(project, buildType.name, "io.flutter:${arch}_$flutterBuildMode:$engineVersion") print("io.flutter:${arch}_$flutterBuildMode:$engineVersion\n"); }}Copy the code

Code explanation:

  1. Setup maven url address of the warehouse, the default is storage.googleapis.com/download.fl… If the network speed is not ideal, you can also configure the FLUTTER_STORAGE_BASE_URL environment variable to point to the domestic mirror address **storage.flutter-io.cn/download.fl…
  2. Add embedded dependenciesio.flutter:flutter_embedding_$flutterBuildMode:$engineVersion, this depends on andBuild a modelandFlutter Engine versionHave a relationship. Example –IO. Flutter: flutter_embedding_debug: c956a31c0a3d350827aee6c56bb63337c5b4e6e 1.0.0-2
  3. addlibflutter.soDependence, this dependence and thetaarchitectureandFlutter Engine versionHave a relationship. Example –IO. Flutter: armeabi_v7a_debug: c956a31c0a3d350827aee6c56bb63337c5b4e6e 1.0.0-2

Tip:

  1. Flutter_embedding endows Flutter with the ability to embed native.
  2. Libflutter. So is the Flutter Engine;
  3. The last thing these two dependencies call isproject.dependencies.add(configuration, dependency, config)This method, so it’s givenprojectPlus dependence. becausesqfliteothermoduleBoth dependencies are required.

Now that the FlutterPlugin/ Apply process is analyzed, let’s analyze the addFlutterTasks method left over from Step 2.

FlutterPlugin/addFlutterTasks

This method takes a lot of code, so let’s summarize it.

private void addFlutterTasks(Project project) { // 1 String target = project.flutter.target if (target == null) { target } // 2 def addFlutterDeps = {variant -> 2.1 FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) { ... } 2.2 Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) { ... 2.3 addApiDependencies} (project, the variant. The name, Project. files {packFlutterAppAotTask}) 2.4 Task copyFlutterAssetsTask = project.tasks. Create (name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy, ) { ... } } // 3 project.android.applicationVariants.all { variant -> ... } // 4 configurePlugins() }Copy the code
  1. Get various parameters from gradle.properties. The example given above – if there is no target configuration, default to lib/main.dart.
  2. Define a function addFlutterDeps.
  • Create the corresponding FlutterTask based on the build pattern and the parameters obtained from gradle.properties in the first step. The function of FlutterTask is to compile the code of the Flutter APP.
  • PackFlutterAppAotTask package FlutterTask compilation results into libs.jar files.
  • Add libs.jar file dependencies to project.
  • CopyFlutterAssetsTask is the asset associated with a Flutter App to copy. Because Flutter may be compiled as a plug-in or subproject, the compilation result is packaged as an AAR if the plug-in is compiled, and the compilation result is packaged as an APK when the subproject is compiled, there is a difference between the two cases.
  1. Add Flutter dependencies to all applicationVariants or libraryVariants, execute the addFlutterDeps function and copy the APK to the target path.
  2. This method adds a Plugin dependency to the project. Dependencies are handled differently depending on how they are compiled.
FlutterTask

FlutterTask’s build() calls its parent’s buildBundle() method:

void buildBundle() { if (! sourceDir.isDirectory()) { throw new GradleException("Invalid Flutter source directory: ${sourceDir}") } intermediateDir.mkdirs() // Compute the rule name for flutter assemble. To speed up builds that contain  // multiple ABIs, the target name is used to communicate which ones are required // rather than the TargetPlatform. This allows multiple builds to share the same // cache. String[] ruleNames; if (buildMode == "debug") { if (fastStart) { ruleNames = ["faststart_android_application"] } else { ruleNames = ["debug_android_application"] } } else { ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" } } project.exec { logging.captureStandardError LogLevel.ERROR executable flutterExecutable.absolutePath workingDir sourceDir if (localEngine ! = null) { args "--local-engine", localEngine args "--local-engine-src-path", localEngineSrcPath } if (verbose) { args "--verbose" } else { args "--quiet" } args "assemble" args "--depfile", "${intermediateDir}/flutter_build.d" args "--output", "${intermediateDir}" if (performanceMeasurementFile ! = null) { args "--performance-measurement-file=${performanceMeasurementFile}" } if (! fastStart || buildMode ! = "debug") { args "-dTargetFile=${targetPath}" } else { args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}" } args "-dTargetPlatform=android" args "-dBuildMode=${buildMode}" if (trackWidgetCreation ! = null) { args "-dTrackWidgetCreation=${trackWidgetCreation}" } if (splitDebugInfo ! = null) { args "-dSplitDebugInfo=${splitDebugInfo}" } if (treeShakeIcons == true) { args "-dTreeShakeIcons=true" } if (dartObfuscation == true) { args "-dDartObfuscation=true" } if (dartDefines ! = null) { args "--DartDefines=${dartDefines}" } if (bundleSkSLPath ! = null) { args "-iBundleSkSLPath=${bundleSkSLPath}" } if (codeSizeDirectory ! = null) { args "-dCodeSizeDirectory=${codeSizeDirectory}" } if (extraGenSnapshotOptions ! = null) { args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}" } if (extraFrontEndOptions ! = null) { args "--ExtraFrontEndOptions=${extraFrontEndOptions}" } args ruleNames } }Copy the code

This method actually executes a FLUTTER build method with some parameters.

APK

Finally, let’s look at the structure of APK.

  1. inlibThe file contains support for 4 architectures by default, includingFlutter Enginelibflutter.so.Flutter App codelibapp.so.The SO file that the Flutter plugin depends onlibijkffmpeg.so,libijkplayer.so,libijksdl.so;

You may encounter couldn’t find “libflutter. So “because there is no libflutter or libapp.so in x86, this is a known problem with flutter.

  1. Flutter AppThe resource files in theassets/flutter_assetsIn the.

AndroidManifest.xml

Let’s take a look at what’s configured in androidmanifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.jj_movie"> // 1. <application android:name="io.flutter.app.FlutterApplication" android:label="jj_movie" android:icon="@mipmap/ic_launcher"> <activity . > // 2 <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> // 3 <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background" /> </activity> // 4 <meta-data android:name="flutterEmbedding" android:value="2" /> </application> </manifest>Copy the code
  1. Project Application such as IO. Flutter. App. FlutterApplication;
  2. By default, a white theme is defined to be visible to the user before the Flutter APP is loaded and displayed, and to be the background of the Window after the Flutter APP is loaded and displayed.
  3. You can modify the startup diagram;
  4. useFlutter Android Embedding V2Version.FlutterActivity.FlutterActivity.FlutterActivityandFlutterActivityAnd so on are inV2Introduced in the version.

FlutterApplication

The code in FlutterApplication is very simple. It mainly executes flutterinjector.instance ().flutterloader ().startInitialization(this).

public class FlutterApplication extends Application { @Override @CallSuper public void onCreate() { super.onCreate(); FlutterInjector.instance().flutterLoader().startInitialization(this); }}Copy the code

The main function of FlutterLoader is to load FLutter Engine and FLutter APP resources, etc. Let’s look at the main code in the startInitialization method of FlutterLoader:

public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
    // 1.
    if (Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("startInitialization must be called on the main thread");
    }

    // 2.
    VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE))
        .init();

    // 3.
    Callable<InitResult> initTask =
        new Callable<InitResult>() {
          @Override
          public InitResult call() {
            ...
          }
        };
  }
Copy the code
  1. Ensure that this method is executed on the main thread;
  2. VsyncWaiter is initialized. Its main function is to register with the Android system and wait for the VSync signal.

VsyncWaiter is the repeater of Flutter rendering. When a signal is received, the Flutter app will be notified to initiate a rendering call, and then perform some layout and paint columns, which will be submitted to the GPU thread for rendering.

  1. Start an asynchronous thread and load some asset resources.

Summary: FlutterLoader performs startInitialization to prepare the Flutter app for loading and rendering.

FlutterActivity

Let’s look at the important code for FlutterActivity:

public class FlutterActivity extends Activity { // 1 protected FlutterActivityAndFragmentDelegate delegate; // 2 private LifecycleRegistry lifecycle; public FlutterActivity() { lifecycle = new LifecycleRegistry(this); } @ Override protected void onCreate (@ Nullable Bundle savedInstanceState) {/ / 3.1 switchLaunchThemeForNormalTheme (); super.onCreate(savedInstanceState); / / 3.2 delegate = new FlutterActivityAndFragmentDelegate (this); delegate.onAttach(this); delegate.onActivityCreated(savedInstanceState); / / 3.3 configureWindowForTransparency (); / / 3.4 the setContentView (createFlutterView ()); / / 3.5 configureStatusBarForFullscreenFlutterExperience (); } private View createFlutterView() { return delegate.onCreateView(null , null, null); }}Copy the code
  1. FlutterActivityAndFragmentDelegateattributedelegateIn charge, so to speakFLutter appRelated to most functional objects;
  2. LifecycleRegistry is a LifeCycle that handles the FlutterActivity LifeCycle, initialized in the constructor;
  3. inonCreateThe main tasks of the method are:
    • First switch to the theme of the startup image to display the startup image
    • Initialize theFlutterActivityAndFragmentDelegateobjectdelegate
    • Then make the window’s background transparent
    • FlutterActivitytheViewAdd thedelegateObject to createFlutterViewAs aFLutter appThe rendering ofView.FlutterViewisSurfaceViewThe subclass.
    • Android 5.0 above set to immersive status bar

So far, everything is ready, just wait FlutterActivityAndFragmentDelegate will load content came in.

FlutterActivityAndFragmentDelegate

  1. The constructor passes FlutterActivity as Host, mainly to get the context.
FlutterActivityAndFragmentDelegate(@NonNull Host host) {
    this.host = host;
  }
Copy the code
  1. onAttachIt’s basically initializedFlutter EngineAnd registeredFlutter plug-in;
void onAttach(@NonNull Context context) {
    ensureAlive();

    if (flutterEngine == null) {
      setupFlutterEngine();
    }
    
    host.configureFlutterEngine(flutterEngine);
  }
Copy the code
// FlutterActivity
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine);
 }
Copy the code

Logic and iOS similar GeneratedPluginRegistrant this registered plug-in.

  1. Load the contents of the Flutter App

Call when FlutterActivity execution onStart FlutterActivityAndFragmentDelegate onStart method, and then from the lib/main dart entry documents began to perform.

void onStart() { doInitialFlutterViewRun(); } private void doInitialFlutterViewRun() { if (flutterEngine.getDartExecutor().isExecutingDart()) { return; } if (host.getInitialRoute() ! = null) { flutterEngine.getNavigationChannel().setInitialRoute(host.getInitialRoute()); } String appBundlePathOverride = host.getAppBundlePath(); if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) { appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath(); } DartExecutor.DartEntrypoint entrypoint = new DartExecutor.DartEntrypoint( appBundlePathOverride, host.getDartEntrypointFunctionName()); flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); }Copy the code