preface

Through the Build Variant capability provided by AGP, we can package individual projects into different APKs or AArs. Build Variant relies on properties and methods provided by BuildType and ProductFlavor to configure a set of rules that combine code and resources.

BuildType I understand is biased towards defining build modes, debug and release are two different build modes, with BuildType we can configure signature information and so on.

ProductFlavor can be understood as product variants. It defines different versions of projects, such as free version and paid version, domestic version and international version, etc. The advantage of this is that only one project needs to be maintained and codes and resources can be reused to the maximum extent.

SourceSet is a set of sources. With SourceSet, you can specify resource paths for different versions. Note that SourceSet is not unique to AGP; the Java Plugin also defines SourceSet.

BuildType

BuildType configates the build types we need, the most common being Debug and Release, to distinguish between development mode and release mode, which AGP creates by default. Of course we can define other build types as well. There are a lot of properties we can configure in the buildTypes closure. What are they? Now let’s look at buildType corresponding class com. Android. Build. Gradle. Internal. DSL. BuildType inheritance structure

Let’s seedefaultConfigThe corresponding classcom.android.build.gradle.internal.dsl.DefaultConfigInheritance structure of

You can see that defaultConfig and buildType eventually inherit from BaseConfigImpl, so the reason why we often feel that a parameter can appear anywhere is because of the inheritance of mapped classes. The properties defined in BaseConfigImpl include the following

public abstract class BaseConfigImpl implements Serializable.BaseConfig {

    private String mApplicationIdSuffix = null;
    private String mVersionNameSuffix = null;
    private final Map<String, ClassField> mBuildConfigFields = Maps.newTreeMap();
    private final Map<String, ClassField> mResValues = Maps.newTreeMap();
    private final List<File> mProguardFiles = Lists.newArrayList();
    private final List<File> mConsumerProguardFiles = Lists.newArrayList();
    private final List<File> mTestProguardFiles = Lists.newArrayList();
    private final Map<String, Object> mManifestPlaceholders = Maps.newHashMap();
    @Nullable
    private Boolean mMultiDexEnabled;

    @Nullable
    private File mMultiDexKeepProguard;

    @Nullable
    privateFile mMultiDexKeepFile; . }Copy the code

The basic use

The properties configured in buildTypes override the definitions in defaultConfig

android {
    defaultConfig {
        manifestPlaceholders = [hostName:"www.example.com"]... } buildTypes { release {minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            applicationIdSuffix ".debug"
            debuggable true}}}Copy the code

BuildType is a good concept to understand, but the specific configuration properties and methods are not listed in the official documentation.

Next we’ll focus on ProductFlavor and SourceSet

ProductFlavor

First let’s look at the relationship between ProductFlavor and DefaultConfig. They both inherit from BaseFlavor. Since ProductFlavor has a higher priority than DefaultConfig, So the properties of DefaultConfig can be overridden in ProductFlavor

Based on using

One of the most common options is productFlavors, which is a variety of options

// Manifestplaceholder = [CHANNEL_VALUE:"xiaomi"]
    }
    huawei{
        manifestPlaceholders = [CHANNEL_VALUE: "huawei"]}}Copy the code

The preceding example overrides the CHANNEL_VALUE configuration parameter in the manifest by defining different flavors to differentiate channels.

productFlavors{
        flavorDimensions 'isFree', free {// minSdkVersion 21 // The free version and the paid version use different package names applicationId'com.example.android.free'// Write a different res value resValue"string".'tag'.'free'// Specify the dimension'isFree'
        }
        paid {
            minSdkVersion 24
            applicationId 'com.example.android.paid'
            resValue "string".'tag'.'paid'
            dimension 'isFree'}}Copy the code

After creating and configuring product variants, click Sync Now in the notification bar. After the synchronization is complete, Gradle automatically creates build variants based on build Type and product variants and names them according to < build-type >. In the above example, Gradle creates the following build variants:

  • freeDebug
  • freeRelease
  • paidDebug
  • paidRelease

flavorDimensions

Note that starting with AGP 3.0, at least one flavor Dimension must be explicitly specified. The way dimension is specified is illustrated in the above example. Some students may arbitrarily assign a dimension to different flavors, but do not know the specific function of this thing.

In effect, Dimension brings together configurations from multiple product variants. In the example above, we define free and paid flavors based on whether they are paid or not. So they should both belong to the same dimension. So we assigned isFree dimension.

If our app also distinguishes between domestic and international versions, then we can also define an Area Dimension as follows

productFlavors{
        flavorDimensions 'isFree'."area"
        free {
            minSdkVersion 21
            applicationId 'com.example.android.free'
            resValue "string".'tag'.'free'
            dimension 'isFree'
        }
        paid {
            minSdkVersion 24
            applicationId 'com.example.android.paid'
            resValue "string".'tag'.'paid'
            dimension 'isFree'
        }

        domestic{
            dimension 'area'
        }
        overseas {
            dimension 'area'}}Copy the code

So by this definition, we have four combinations

  • freeDomesticFree Domestic version
  • freeOverseasFree International Edition
  • paidDomesticPaid domestic version
  • paidOverseasPaid International

One thing to note here is that the order of flavor combinations is based on the order of the elements of flavorDimensions. What if we were to

Reverse the order of isFree and area

flavorDimensions "area".'isFree'
Copy the code

So the original freeDomestic will become domesticFree. What effect would that have?

The first flavor actually has a high priority. Suppose free and domestic have their own package names defined

productFlavors{
        flavorDimensions "area".'isFree'
        free {
            applicationId 'com.example.android.free'
            dimension 'isFree'
        }

        domestic{
            dimension 'area'
            applicationId 'com.example.android.domestic'}}Copy the code

So the final package will be com. Example. Android. Domestic.

matchingFallbacks

In some cases, the app module contains flavors that the library module doesn’t. In that case, the app can’t match the library flavor, specifying matchingFallbacks. For example, in the following example, the app relies on library

//app build.gradle
productFlavors{
        flavorDimensions 'isFree'
        free {
            dimension 'isFree'
            matchingFallbacks = ['demo']
        }
        paid {
            dimension 'isFree'
        }
    }

//library build.gradle
productFlavors{
        flavorDimensions 'isFree'
        demo {
            dimension 'isFree'
        }
        paid {
            dimension 'isFree'}}Copy the code

When executing assembleFreeRelease, the library does not have free flavor, so demo is used instead.

If the app does not specify matchingFallbacks, it will fail to compile and will report the following error

> Could not resolve all artifacts for configuration ':app:freeDebugCompileClasspath'.
   > Could not resolve project :library.
     Required by:
         project :app
Copy the code

So, if the Library and app both define ProductFlavor, you need to align it, otherwise you need to specify matchingFallbacks for pockets. Note that library and app need to be defined in the same dimension.

SourceSet

SourceSet is the source code set, and we can use the SourceSet block to change where Gradle collects the files for each component of the source code set. So we don’t have to change the location of the file. In other words, with SourceSet, we can specify the path of code and resources according to our preferences

The basic use

attribute

Here are the attributes provided by AndroidSourceSets

Property Description
aidl Android AIDL directory
assets Assets directory
java Java directory
jni JNI directory
jniLibs JNI libs directory
manifest AndroidManifest path
name Name of the source set
renderscript RenderScript directory
res Res Resource Directory
resources Java resources directory

Above the configuration of the corresponding is AndroidSourceFile object, in addition to the manifest as a single file, the rest are AndroidSourceDirectorySet object, we’ll look at AndroidSourceDirectorySet interface provides what method.

public interface AndroidSourceDirectorySet extends PatternFilterable {

    @NonNull
    String getName(a);

    // Add the resource path to the collection, and eventually AGP will fetch all the files from the collection
    @NonNull
    AndroidSourceDirectorySet srcDir(Object srcDir);

    // Add multiple resource paths to the collection
    @NonNull
    AndroidSourceDirectorySet srcDirs(Object... srcDirs);

    // Specify the path of the resource. This method overwrites the original collection when different from the above two methods
    @NonNull
    AndroidSourceDirectorySet setSrcDirs(Iterable
        srcDirs);

    // Return the resource as FileTree
    @NonNull
    FileTree getSourceFiles(a);

    // Returns the filter rule
    @NonNull
    PatternFilterable getFilter(a);

    // Return the source folder as a list
    @NonNull
    List<ConfigurableFileTree> getSourceDirectoryTrees(a);

    // Returns a list of resource files
    @NonNull
    Set<File> getSrcDirs(a);

    /** Returns the [FileCollection] that represents this source sets. */
    @Incubating
    FileCollection getBuildableArtifact(a);
}
Copy the code

So we can change the location of the source set. Let’s look at a simple configuration


    def basePath = projectDir.parentFile.absolutePath
    def resPath = new File(basePath, "res")
    def manifestPath = new File(basePath, "AndroidManifest.xml")
    sourceSets {
        main {
            res.srcDir(resPath)
            manifest.srcFile(manifestPath)
        }
    }
Copy the code

We can print the specific configuration through the sourceSets task

:app:sourceSets

/ / output

main
----
Compile configuration: compile
build.gradle name: android.sourceSets.main
Java sources: [app/src/main/java]
//AndroidMnaifest path changed to app root directory
Manifest file: AndroidManifest.xml
// Look at the res directory you added just now
Android resources: [app/src/main/res, res]
Assets: [app/src/main/assets]
AIDL sources: [app/src/main/aidl]
RenderScript sources: [app/src/main/rs]
JNI sources: [app/src/main/jni]
JNI libraries: [app/src/main/jniLibs]
Java-style resources: [app/src/main/resources]

paid
----
Compile configuration: paidCompile
build.gradle name: android.sourceSets.paid
Java sources: [app/src/paid/java]
Manifest file: app/src/paid/AndroidManifest.xml
Android resources: [app/src/paid/res]
Assets: [app/src/paid/assets]
AIDL sources: [app/src/paid/aidl]
RenderScript sources: [app/src/paid/rs]
JNI sources: [app/src/paid/jni]
JNI libraries: [app/src/paid/jniLibs]
Java-style resources: [app/src/paid/resources]

// omit the other source sets.Copy the code

methods

methods describe
setRoot(path) Sets the root of the source set to the given path. All entries for the source collection are located under this root directory.

Using the setRoot method, we can directly specify the directory of a particular source set. For example, if you have multiple productFlavors and have created corresponding source sets, we can group all the non-main directories together to avoid too many files in the SRC directory.

sourceSets.all { set ->
        if(set.name.toLowerCase().contains(flavor) && ! set.name.equals("main")) {
            set.setRoot("src/other/$flavor")}}Copy the code

The source collection types

The main source set contains the code and resources shared by all the other build variants, that is, all the other build variants, SRC/Main being jointly owned.

Other source-set directories are optional and can be created if we want to add specific code or resources for a single build variant. For example, to build the “demoDebug” variant, Gradle looks at the following directories and assigns them the following priorities

  1. src/demoDebug/(Build variant source code set)
  2. src/debug/(Build type source code set)
  3. src/demo/(Product variant source code set)
  4. src/main/(Main source code set)

When there are duplicate resources, Gradle decides which files to use in the following order of precedence (the left source set replaces the files and Settings of the right source set) :

Build variants > Build types [BuildType] > Product Flavors [ProductFlavor] > Main source set [main] > library dependenciesCopy the code
  • All source code in the Java/directory is compiled together to produce a single output

    Java files cannot be overwritten. If we create SRC /main/ utility. Java in the main directory, we cannot overwrite a file with the same name in another sourceset directory because Gradle looks at these directories and throws a “duplicate class” error during the build process. If we wanted to have different versions of Utility. Java for different build types, we would have to have each build type define its own file version, which would be a bit cumbersome.

  • All manifests are merged into one Manifest. The merge priority is the same as mentioned above.

  • Similarly, files in the values/ directory are merged together. If two files have the same name, such as two strings. XML files, override the priority as described above.

  • Resources in the res/ and asset/ directories are packaged together.

  • Finally, when building APK, Gradle assigns the lowest priority to the resources and listings that come with library module dependencies.

Configuring filtering Rules

Review the above AndroidSourceDirectorySet interface, its inherited PatternFilterable interface

public interface PatternFilterable {

    Set<String> getIncludes(a);

    Set<String> getExcludes(a);

    PatternFilterable setIncludes(Iterable<String> includes);

    PatternFilterable setExcludes(Iterable<String> excludes);

    PatternFilterable include(String... includes);

    PatternFilterable include(Iterable<String> includes);

    PatternFilterable include(Spec<FileTreeElement> includeSpec);

    PatternFilterable include(Closure includeSpec);

    PatternFilterable exclude(String... excludes);

    PatternFilterable exclude(Iterable<String> excludes);

    PatternFilterable exclude(Spec<FileTreeElement> excludeSpec);

    PatternFilterable exclude(Closure excludeSpec);

Copy the code

This interface provides a series of include and exclude methods that allow you to do some filtering on the source set directory.

sourceSets {
        main {
            java {
                exclude 'com/cooke/library/Test.java'
                exclude 'com/cooke/library/model/**.java'}}}Copy the code

As mentioned in the previous example, other SourceSet directories cannot override Java files with the same name, but we can exclude Java from the main directory by SourceSet.

Note:includeandexcludeDoes not apply to res. If you want to filter res, you need to pass the definitionres/raw/keep.xml, as shown in theAndroid documentI’m not going to expand it here.

Variant

Variant refers to Variant, which can be divided into ApplicationVariant and LibraryVariant, corresponding to apk Variant and aar Variant respectively. Variants are built by combining BuildType and ProductFlavor. namely

variant = buildType * productFlavor
Copy the code

For example, we defined two productFlavors, free and paid, and combined debug and release buildTypes to produce four combinations, as shown in the figure below

We can intervene in the process of building APK and AAR by iterating through the ApplicationVariant or LibraryVariant lists.

The most common is to rename apK.

android.applicationVariants.all {
        variant ->
            variant.outputs.all {
                outputFileName = "${applicationId}_${buildType.name}_v${defaultConfig.versionName}_${releaseTime()}.apk"}}Copy the code

Select a Variant

AGP creates a set of Variant tasks for each variant. For example, the APK package corresponds to Assemble $VariantName

The aAB package corresponds to the bundle$VariantName. If you need to select a Variant during development, you can change it in the Build Vairants window.

reference

Configure build variants

Android Studio Set of Source code

BuildTypes – android gradle