在Flutter項(xiàng)目下安卓flavor打包配置實(shí)踐

1.前言

Flutter是Google這幾年大力推廣的跨平臺(tái)UI框架,可以快速在iOS和Android上構(gòu)建高質(zhì)量的原生用戶(hù)界面。在架構(gòu)搭建階段,我們依然需要原生技術(shù)的支持。比如說(shuō),我們?cè)陂_(kāi)發(fā)Android項(xiàng)目時(shí),會(huì)通過(guò)在gradle文件中配置Flavor來(lái)實(shí)現(xiàn)不同渠道的屬性配置,之后通過(guò)在編譯過(guò)程中自動(dòng)生成BuildConfig文件來(lái)讀取不同F(xiàn)lavor下的各種屬性。在Flutter項(xiàng)目中,我們?nèi)绾螌?shí)現(xiàn)不同F(xiàn)lavor下讀取相應(yīng)屬性并實(shí)現(xiàn)多渠道打包呢?以及Flutter的打包過(guò)程跟Android原生打包有什么不同呢?
本文將以Flutter1.22.4版本為基礎(chǔ),通過(guò)Flutter項(xiàng)目中對(duì)于Android工程的構(gòu)建流程詳解,來(lái)進(jìn)行Flavor配置說(shuō)明以及Apk構(gòu)建過(guò)程的詳細(xì)分析。并針對(duì)SDK的bug提出了解決方案及原因梳理。

2.Flavor配置及打包

2.1Flavor配置

在移動(dòng)開(kāi)發(fā)中,我們通常需要配置三個(gè)開(kāi)發(fā)環(huán)境,分別為開(kāi)發(fā)環(huán)境,測(cè)試環(huán)境生產(chǎn)環(huán)境。不同的環(huán)境配置不同的Host,及第三方sdk的id,如分享,推送等功能。為了方便測(cè)試在同一個(gè)手機(jī)安裝不同環(huán)境的apk,還會(huì)為不同環(huán)境配置不同包名。配置方式如下:
首先在gradle文件中添加productFlavors,同其他的Android工程的配置方式:

productFlavors {
    // 生產(chǎn)環(huán)境
    flavoronline {
        applicationId "com.sohu.flavor"
    }
    // 開(kāi)發(fā)環(huán)境
    flavordev {
        applicationId "com.sohu.flavor.dev"
    }
    // 測(cè)試環(huán)境
    flavortest {
        applicationId "com.sohu.flavor.test"
    }
}

需要注意的是,所有的Flavor名稱(chēng)都需要是小寫(xiě)的,原因之后的章節(jié)會(huì)進(jìn)行說(shuō)明。

2.2為不同F(xiàn)lavor配置入口文件

Flutter項(xiàng)目并沒(méi)有類(lèi)似BuildConfig這樣自動(dòng)生成不同F(xiàn)lavor下的配置文件。如果讀取Android層的BuildConfig文件需要通過(guò)MethodChannel,異步獲取。在現(xiàn)實(shí)場(chǎng)景中,冷啟動(dòng)的時(shí)候就需要根據(jù)Flavor上報(bào)一些數(shù)據(jù),或進(jìn)行業(yè)務(wù)處理。所以在不同的Flavor下,F(xiàn)lutter建議在App初始化時(shí)指定不同的入口文件來(lái)進(jìn)行不同配置的實(shí)現(xiàn),實(shí)現(xiàn)方式如下:
我們知道默認(rèn)情況下,F(xiàn)lutter工程的入口文件是為lib/main.dart。為了實(shí)現(xiàn)多入口,我們需要寫(xiě)三個(gè)Flavor的入口文件來(lái)替代main.dart。分別為:
1.main_android_dev.dart

import 'package:flutter/material.dart';
import 'package:supermarie/common/config/app_config.dart';
import 'package:supermarie/my_app.dart';
 
void main() {
  AppConfig.init(ConfigType.androidDev);
  runApp(MyAppDev());
}
 
class MyAppDev extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyApp();
  }
}

2.main_android_test.dart

import 'package:flutter/material.dart';
import 'package:supermarie/common/config/app_config.dart';
import 'package:supermarie/my_app.dart';
 
void main() {
  AppConfig.init(ConfigType.androidTest);
  runApp(MyAppTest());
}
 
class MyAppTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyApp();
  }
}

3.main_android_online.dart

import 'package:flutter/material.dart';
import 'package:supermarie/common/config/app_config.dart';
import 'package:supermarie/my_app.dart';
 
void main() {
  AppConfig.init(ConfigType.androidOnline);
  runApp(MyAppOnline());
}
 
class MyAppOnline extends StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
    return MyApp();
  }
}

其中AppConfig.init()方法為根據(jù)不同F(xiàn)lavor進(jìn)行的初始化操作。如:配置Host,各種id,key等。接著再執(zhí)行'runApp()'方法。而MyApp()是走完配置后統(tǒng)一的程序入口,即App的首頁(yè)展示。
其結(jié)構(gòu)如圖所示:

截屏2021-02-01 下午2.27.12.png

2.3 不同F(xiàn)lavor及不同入口的打包

Flavor和入口文件配置完成后,我們可以嘗試打包了,打包方式如下:
flutter build apk --[debug/release] --flavor [flavor] -t [entrance]
參數(shù)說(shuō)明:
[debug/release]:指定debug或release,
[flavor]:指定Flavor,
[entrance]:指定入口文件名稱(chēng)。
所以三個(gè)Flavor,總共六個(gè)包的打包令分別為:

//開(kāi)發(fā)環(huán)境:
flutter build apk --debug --flavor flavordev -t lib/main_android_dev.dart
flutter build apk --release --flavor flavordev -t lib/main_android_dev.dart
//測(cè)試環(huán)境:
flutter build apk --debug --flavor flavortest -t lib/main_android_test.dart
flutter build apk --release --flavor flavortest -t lib/main_android_test.dart
//生產(chǎn)環(huán)境:
flutter build apk --debug --flavor flavoronline -t lib/main_android_online.dart
flutter build apk --release --flavor flavoronline -t lib/main_android_online.dart

如果一切正常的情況下,我們就會(huì)在項(xiàng)目根目錄/build/app/outputs/app/[flavor]/[debug/release]下看到生成的apk文件了。
好,F(xiàn)lutter項(xiàng)目下Android工程的Flavor設(shè)置及打包已經(jīng)完成了。下一章將通過(guò)源碼解析來(lái)梳理Flutter項(xiàng)目Android包的打包流程。

3.Flutter Android構(gòu)建過(guò)程詳解

Flutter項(xiàng)目的構(gòu)建入口文件位于:
fluttersdk/packages/flutter_tools/bin下的flutter_tools.dart文件,這是所有平臺(tái)的構(gòu)建入口,代碼如下:

import 'package:flutter_tools/executable.dart' as executable;

void main(List<String> args) {
  executable.main(args);
}

main()函數(shù)里只有一行代碼,執(zhí)行了executable.dart的main()方法,我們繼續(xù)看它的實(shí)現(xiàn):

//skip 
//...

  await runner.run(args, () => <FlutterCommand>[
//skip 
//...
    BuildCommand(verboseHelp: verboseHelp),
//skip 
//...
  ], verbose: verbose,
     muteCommandLogging: muteCommandLogging,
     verboseHelp: verboseHelp,
     overrides: <Type, Generator>{
//skip 
//...
     });
}

這段代碼比較長(zhǎng),會(huì)初始化很多command指令,然后再依次執(zhí)行。但由于我們分析的是apk的構(gòu)建流程,所以我們只看BuildCommand()的執(zhí)行過(guò)程。

  BuildCommand({ bool verboseHelp = false }) {
    addSubcommand(BuildAarCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildAotCommand());
    addSubcommand(BuildIOSCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildIOSFrameworkCommand(
      buildSystem: globals.buildSystem,
      verboseHelp: verboseHelp,
    ));
    addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildWebCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildMacosCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildLinuxCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildWindowsCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildFuchsiaCommand(verboseHelp: verboseHelp));
  }

BuildCommand()中添加了很多子command指令,用于不同平臺(tái)或生成文件的構(gòu)建。由于我們分析的是apk的打包,接下來(lái)繼續(xù)看BuildApkCommand()的實(shí)現(xiàn),位于fluttersdk/packages/flutter_tools/lib/src/commands/build_apk.dart

class BuildApkCommand extends BuildSubCommand {
  BuildApkCommand({bool verboseHelp = false}) {
    addTreeShakeIconsFlag();
    usesTargetOption();
//skip
//...
  }
//skip
//...

  @override
  Future<FlutterCommandResult> runCommand() async {
//skip
//...
    await androidBuilder.buildApk(
      project: FlutterProject.current(),
      target: targetFile,
      androidBuildInfo: androidBuildInfo,
    );
    return FlutterCommandResult.success();
  }
}

我們看到BuildApkCommand()的構(gòu)造方法先進(jìn)行了一些初始化配置?;氐街?code>executable.main()方法,如果我們繼續(xù)跟蹤runner.run()方法,執(zhí)行command的實(shí)現(xiàn),實(shí)際上就是執(zhí)行各個(gè)command以及其子command的代碼,在執(zhí)行到BuildApkCommand()時(shí),調(diào)用了其runCommand()方法,如上述代碼所示。這個(gè)方法的核心實(shí)現(xiàn)就是androidBuilder.buildApk(),我們接著看androidBuilder.buildApk()的實(shí)現(xiàn):

  @override
  Future<void> buildApk({
    @required FlutterProject project,
    @required AndroidBuildInfo androidBuildInfo,
    @required String target,
  }) async {
    try {
      await buildGradleApp(
        project: project,
        androidBuildInfo: androidBuildInfo,
        target: target,
        isBuildingBundle: false,
        localGradleErrors: gradleErrors,
      );
    } finally {
      globals.androidSdk?.reinitialize();
    }
  }

其中核心代碼buildGradleApp()方法的位置在fluttersdk/packages/flutter_tools/lib/src/android/gradle.dart下。從文件的命名中即可得知,這個(gè)文件跟Android的gradle文件是密切相關(guān)的。我們截取buildGradleApp()方法核心部分的實(shí)現(xiàn):

Future<void> buildGradleApp({
  @required FlutterProject project,
  @required AndroidBuildInfo androidBuildInfo,
  @required String target,
  @required bool isBuildingBundle,
  @required List<GradleHandledError> localGradleErrors,
  bool shouldBuildPluginAsAar = false,
  int retries = 1,
}) async {
//skip
//...
  // The default Gradle script reads the version name and number
  // from the local.properties file.
  updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
//skip
//...

  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
  final String assembleTask = isBuildingBundle
    ? getBundleTaskFor(buildInfo)
    : getAssembleTaskFor(buildInfo);

  final Status status = globals.logger.startProgress(
    "Running Gradle task '$assembleTask'...",
    timeout: timeoutConfiguration.slowOperation,
    multilineOutput: true,
  );

  final List<String> command = <String>[
    gradleUtils.getExecutable(project),
  ];
//skip
//...
  command.add(assembleTask);
//skip
//...
  final Stopwatch sw = Stopwatch()..start();
  int exitCode = 1;
  try {
    exitCode = await processUtils.stream(
      command,
      workingDirectory: project.android.hostAppGradleRoot.path,
      allowReentrantFlutter: true,
      environment: gradleEnvironment,
      mapFunction: consumeLog,
    );
  } on ProcessException catch (exception) {
    consumeLog(exception.toString());
    // Rethrow the exception if the error isn't handled by any of the
    // `localGradleErrors`.
    if (detectedGradleError == null) {
      rethrow;
    }
  } finally {
    status.stop();
  }
//skip
//...

  // Gradle produced an APK.
  final Iterable<String> apkFilesPaths = project.isModule
    ? findApkFilesModule(project, androidBuildInfo)
    : listApkPaths(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
  final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
  if (!apkFile.existsSync()) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }

//skip
//...

從代碼中我們可得知,在buildGradleApp()方法中會(huì)讀取并修改local.properties的屬性,local.properties是根據(jù)Flutter的pubspec.yaml文件等配置生成的本地文件。然后根據(jù)buildInfo獲取assembleTask,設(shè)置當(dāng)前的編譯狀態(tài)status,初始化command數(shù)組,并將assembleTask加入其中。接著執(zhí)行command列表,其中一個(gè)command即調(diào)用gradle執(zhí)行assembleTask。執(zhí)行完成后,生成apk文件,進(jìn)行構(gòu)建檢查,然后構(gòu)建結(jié)束。
既然執(zhí)行到gradle文件了,我們?cè)賮?lái)看看Flutter項(xiàng)目下的gradle文件和普通Android工程的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'
}

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

這是截取項(xiàng)目中的gradle文件,不同之處都在頭部。首先是讀取localProperties的屬性,主要判斷flutter sdk是否存在,以及獲取app的versionCodeversionName。然后在執(zhí)行項(xiàng)目其他gradle配置之前,先執(zhí)行sdk中的flutter.gradle文件。位置在:fluttersdk/packages/flutter_tools/gradle/flutter.gradle。其結(jié)構(gòu)如下:

截屏2021-02-01 下午3.52.58.png

flutter.gradle這個(gè)文件代碼很長(zhǎng),但從這張圖上,我們可以很清晰的看出結(jié)構(gòu)。主要任務(wù)就是執(zhí)行了FlutterPlugin,在FlutterPlugin中定義了FlutterTask這個(gè)任務(wù),而FlutterTask必然承載了Flutter的編譯過(guò)程。FlutterTask的核心代碼如下:

    @TaskAction
    void build() {
        buildBundle()
    }

buildBundle()方法就是根據(jù)不同的配置,執(zhí)行了各種flutter的編譯指令,比如之前示例代碼中,我們通過(guò)-t指定flutter層的程序入口。由于這部分已經(jīng)出離本文的討論范圍,先不做展開(kāi)了。大家只要知道這部分執(zhí)行完成后最終生成了Flutter層的編譯產(chǎn)物即可。
經(jīng)過(guò)了原生層和Flutter層的構(gòu)建,整個(gè)Flutter項(xiàng)目的Android apk構(gòu)建流程就梳理完畢了。構(gòu)建思路已經(jīng)非常清晰,但是在打包過(guò)程中,我們發(fā)現(xiàn)了新的問(wèn)題,使我們對(duì)構(gòu)建流程的一些細(xì)節(jié)進(jìn)行了梳理,下一章將對(duì)打包過(guò)程中的bug和原因進(jìn)行說(shuō)明。

4.Flutter SDK打包bug說(shuō)明

我們回到Flavor設(shè)置及打包這個(gè)章節(jié)的開(kāi)始的Flavor配置示例:

productFlavors {
    // 生產(chǎn)環(huán)境
    flavoronline {
        applicationId "com.sohu.flavor"
    }
    // 開(kāi)發(fā)環(huán)境
    flavordev {
        applicationId "com.sohu.flavor.dev"
    }
    // 測(cè)試環(huán)境
    flavortest {
        applicationId "com.sohu.flavor.test"
    }
}

在最一開(kāi)始的配置示例中,我們說(shuō)明了在gradle文件中配置的Flavor名稱(chēng)必須是小寫(xiě)的。但實(shí)際上一開(kāi)始我們是用的駝峰命名,即:flavorOnline, flavorDev, flavorTest。
這是因?yàn)槲覀冮_(kāi)發(fā)過(guò)程是在macOS系統(tǒng)下進(jìn)行的,打包是在Ubuntu系統(tǒng)下通過(guò)jenkins打包完成的。在開(kāi)發(fā)過(guò)程中,我們打各個(gè)環(huán)境下的包都完全沒(méi)有問(wèn)題,但是在Ubuntu系統(tǒng)下,會(huì)報(bào)如下錯(cuò)誤:

Gradle build failed to produce an .apk file. It's likely that this file was generated under XXX(目錄), but the tool couldn't find it.

通常情況下,這個(gè)問(wèn)題的出現(xiàn)是因?yàn)樵诙嗲来虬鼤r(shí),打包命令沒(méi)有指定相應(yīng)的入口文件或渠道名稱(chēng),但實(shí)際上我們的打包命令是沒(méi)有問(wèn)題的,而且我們?cè)赬XX目錄下找到了apk文件,這些apk文件在安裝運(yùn)行過(guò)程中也都沒(méi)有任何問(wèn)題。那么這個(gè)問(wèn)題是怎么出現(xiàn)的呢?
先說(shuō)結(jié)論,經(jīng)過(guò)驗(yàn)證,這個(gè)是flutter sdk(1.22.4)的一個(gè)bug。
再說(shuō)明原因之前,說(shuō)明一下針對(duì)此問(wèn)題的三種可行解決方案。
1.不作任何處理,每次安裝apk包都通過(guò)adb install命令去安裝。jenkins上雖然每次都會(huì)提示打包失敗,但實(shí)際上打包文件都在,也都會(huì)顯示在頁(yè)面中。
2.Flavor名稱(chēng)全小寫(xiě),也就是我們最終的實(shí)現(xiàn)方案。
3.修改flutter sdk中的gradle.dart文件第488行為:

final File apkFile = apkDirectory.childFile(apkFilesPaths.first.toLowerCase());

接下來(lái)我們分析一下問(wèn)題產(chǎn)生的原因:
比如我們打一個(gè)debug flavorDev的包,打包完成后在XXX目錄下為:app-flavordev-debug.apk
我們回到gradle.dart文件,看看打包完成后的執(zhí)行內(nèi)容,主要是做一些檢查工作:

  // Gradle produced an APK.
  final Iterable<String> apkFilesPaths = project.isModule
    ? findApkFilesModule(project, androidBuildInfo)
    : listApkPaths(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
  final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
  if (!apkFile.existsSync()) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }
void _exitWithExpectedFileNotFound({
  @required FlutterProject project,
  @required String fileExtension,
}) {
  assert(project != null);
  assert(fileExtension != null);
 
  final String androidGradlePluginVersion =
  getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot);
  BuildEvent('gradle-expected-file-not-found',
    settings:
    'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension',
    flutterUsage: globals.flutterUsage,
  ).send();
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
    "It's likely that this file was generated under ${project.android.buildDirectory.path}, "
    "but the tool couldn't find it."
  );
}

其中apkFilesPaths是需要檢查的apk路徑,通過(guò)apkFilesPaths拿到apkFile文件名稱(chēng),接著對(duì)apkFile文件進(jìn)行檢查,如果不存在則報(bào)如上所述異常。
我們看看apkFilesPaths是如何生成的,由于project.isModulefalse,所以直接看listApkPaths()的實(shí)現(xiàn):

Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
      androidBuildInfo.buildInfo.flavor,
    '$buildType.apk',
  ];
  if (androidBuildInfo.splitPerAbi) {
    return <String>[
      for (AndroidArch androidArch in androidBuildInfo.targetArchs)
        <String>[
          'app',
          getNameForAndroidArch(androidArch),
          ...apkPartialName
        ].join('-')
    ];
  }
  return <String>[
    <String>[
      'app',
      ...apkPartialName,
    ].join('-')
  ];
}

這段代碼說(shuō)明檢查的apk的命名方式為:'app-' + androidBuildInfo.buildInfo.flavor + '$buildType.apk'。所以apkFilesPaths.first的結(jié)果如下:

app-flavorDev-debug.apk

回過(guò)頭來(lái)看XXX目錄下的apk文件:app-flavordev-debug.apk,發(fā)現(xiàn)差了一個(gè)大小寫(xiě)。所以猜測(cè)是大小寫(xiě)問(wèn)題導(dǎo)致的。我們修改gradle.dart的以下代碼:

final File apkFile = apkDirectory.childFile(apkFilesPaths.first);

為:

final File apkFile = apkDirectory.childFile(apkFilesPaths.first.toLowerCase());

即檢查的apkFile的文件名稱(chēng)強(qiáng)制轉(zhuǎn)成小寫(xiě),發(fā)現(xiàn)build成功不會(huì)再報(bào)錯(cuò)。
那為什么在macOS就不會(huì)報(bào)錯(cuò)呢?
大概是因?yàn)椴煌琽s在針對(duì)檢查文件一致性的方式不同。Ubuntu系統(tǒng)下會(huì)針對(duì)文件名稱(chēng)大小寫(xiě)進(jìn)行嚴(yán)格檢查。

注意:為了讓Flutter sdk修改的內(nèi)容生效,修改代碼后需刪除fluttersdk/flutter/bin/cache路徑下的flutter_tools.snapshotflutter_tools.stamp重新編譯sdk。

另外,我們知道在gradle文件下可以指定輸出的文件名稱(chēng),這個(gè)是沒(méi)有問(wèn)題的。但是flutter sdk中的flutter.gradle(fluttersdk/packages/flutter_tools/gradle/flutter.gradle)依舊會(huì)在XXX目錄下輸出相應(yīng)的小寫(xiě)文件,此處可看flutter.gradle的相應(yīng)實(shí)現(xiàn):

variant.outputs.all { output ->
    // `assemble` became `assembleProvider` in AGP 3.3.0.
    def assembleTask = variant.hasProperty("assembleProvider")
        ? variant.assembleProvider.get()
        : variant.assemble
    assembleTask.doLast {
//skip
//...
        if (variant.flavorName != null && !variant.flavorName.isEmpty()) {
            filename += "-${variant.flavorName.toLowerCase()}"
        }
        filename += "-${buildModeFor(variant.buildType)}"
        project.copy {
            from new File("$outputDirectoryStr/${output.outputFileName}")
            into new File("${project.buildDir}/outputs/flutter-apk");
            rename {
                return "${filename}.apk"
            }
        }
    }
}

所以為了保證打包成功,在不修改sdk源碼的情況下,我們需將Flavor設(shè)置成小寫(xiě)即可繞過(guò)這個(gè)bug。
最后說(shuō)明一下,目前在Flutter sdk的master分支上,此bug已修復(fù)。在gradle.dart文件中,會(huì)檢查小寫(xiě)文件,listApkPaths()的實(shí)現(xiàn)如下:

Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
      androidBuildInfo.buildInfo.lowerCasedFlavor,
    '$buildType.apk',
  ];
//skip
//...
}

可以看到apkPartialName返回的第1位是:androidBuildInfo.buildInfo.lowerCasedFlavor,在取Flavor的時(shí)候,直接取小寫(xiě)屬性。
所以大家也可以耐心等待,新的穩(wěn)定版發(fā)布后,這個(gè)bug應(yīng)該就fix了。

5.總結(jié)

經(jīng)過(guò)實(shí)踐,最終我們的Flutter項(xiàng)目Android端,使用以上方式進(jìn)行了Flavor配置與屬性讀取。并且本文通過(guò)源碼解析梳理了整個(gè)構(gòu)建流程,希望能夠幫助大家更好的理解Flutter是如何進(jìn)行apk構(gòu)建的。另外在實(shí)踐過(guò)程中,發(fā)現(xiàn)了sdk在打包過(guò)程中的一個(gè)bug,且給出了解決方案可以在不修改源碼的情況下繞過(guò)此問(wèn)題。

6.參考文獻(xiàn)

http://www.itdecent.cn/p/b9e7c00075e1?from=timeline&isappinstalled=0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容