1. Settings file

Gradle defines a Settings file called settings. Gradle, which is placed in the root project directory. Settings. Gradle is usually used to configure sub-modules. Note that a submodule will only be included in the build if it is configured in Settings. gradle.

Create a new project called AndroidConfig

If you look at Settings. gradle, you can see that the project name and app Module are configured by default

rootProject.name = "AndroidConfig"
include ':app'
Copy the code

Include calls methods on the Settings interface

void include(String... str);
Copy the code

In addition to placing the Module in the same directory as the root directory, you can also place the Module in any directory, such as the Core Module in the ARouter directory

rootProject.name = "AndroidConfig"
include ':app'

include ':Core'
project(':Core').projectDir = new File(rootDir, 'ARouter/Core')
Copy the code

Results the following

2. Unified version management

After n iterations, the project tends to become larger and larger, and modularity is essential. Therefore, it may occur that multiple modules depend on multiple versions of the same library, which may cause some unnecessary trouble and require unified version management.

Gradle allows you to add custom properties using Ext, such as Ext.age = 18. When you add multiple custom properties, you add them using ext blocks.

Based on the project AndroidConfig, create a config.gradle file in the root directory of the project, which is used to unify version management, and the following contents are compiled

ext {
    android = [
            "compileSdkVersion"            : 30."minSdkVersion"                : 21."targetSdkVersion"             : 28."versionCode"                  : 1."versionName"                  : "1.0"."appcompat"                    : "1.2.0"."glide"                        : "4.12.0",
    ]

    dependencies = [
            "appcompat"                    : "androidx.appcompat:appcompat:${android["appcompat"]}"."glide"                        : "com.github.bumptech.glide:glide:${android["glide"]}"."glide-compiler"               : "com.github.bumptech.glide:compiler:${android["glide"]}"]}Copy the code

Then apply the script to the project root directory build.gradle so that all modules can refer to the custom properties in it

apply from: "config.gradle"
Copy the code

Use these custom properties in submoduel, such as build.gradle under app Module in the code below

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    defaultConfig {
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]
    }

}

dependencies {
    implementation fileTree(include: ['*.jar'].dir: 'libs')
    //Android Library
    implementation rootProject.ext.dependencies["appcompat"]
    //Glide
    implementation rootProject.ext.dependencies["glide"]
    annotationProcessor rootProject.ext.dependencies["glide-compiler"]}Copy the code

When you need to change the version of a dependent library, you can modify it directly in config.gradle, which is very easy to manage

Iii. The version number of the release package increases by 1 each time

Based on the project AndroidConfig, create a version.properties file in the project root directory to record the version number and encode the following contents

VERSION_CODE=1
Copy the code

Compile the following code in build.gradle under app Module

def versionCodeAutoIncrement() {
    def file = file('.. /version.properties')
    if (file.canRead()) {
        def properties = new Properties()
        properties.load(new FileInputStream(file))
        def versionCode = properties['VERSION_CODE'].toInteger()
        def taskNames = gradle.startParameter.taskNames
        // Incrementing the release package
        if (taskNames.contains(":app:assembleRelease")) {
            versionProps['VERSION_CODE'] = (++versionCode).toString()
            versionProps.store(versionFile.newWriter(), null)}return versionCode
    } else {
        throw new GradleException("Could not find version.properties!")
    }
}

android {
    defaultConfig {
        applicationId "com.example.androidconfig"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode versionCodeAutoIncrement()
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}}Copy the code

When packaging, the version number in the version.properties file is read, and then the version number is automatically +1 and rewritten to the version.properties file.

4. Modify the apK/AAR file name generated by the package

The Android object provides three properties, as shown in the table. A variant is a cross between a build type and a product variant. For example, the ApplicationVariant can represent a release package, a debug package, and so on.

It is important to note that accessing these three properties triggers the creation of all tasks without reconfiguration. Using this feature, by accessing them and changing the APK/AAR file name, the creation of all tasks is automatically triggered, and the modification of the APK/AAR file name takes effect, achieving the purpose of changing the APK/AAR file name.

variant instructions
applicationVariants Gradle plugin for Android applications only
libraryVariants Only works with the Android library Gradle plugin
testVariants Both Gradle plugins work

By default, the source code is not visible. Add a dependency to the app Module’s build.gradle, click Sync, and wait for the source code to be downloaded

dependencies {
    compileOnly "Com. Android. Tools. Build: gradle: 2"
}
Copy the code

Start by looking at the source code for these three properties

//applicationVariants
public abstract class AbstractAppExtension public constructor(dslServices: com.android.build.gradle.internal.services.DslServices, globalScope: com.android.build.gradle.internal.scope.GlobalScope, buildOutputs: org.gradle.api.NamedDomainObjectContainer<com.android.build.gradle.api.BaseVariantOutput>, sourceSetManager: com.android.build.gradle.internal.dependency.SourceSetManager, extraModelInfo: com.android.build.gradle.internal.ExtraModelInfo, isBaseModule: kotlin.Boolean) : com.android.build.gradle.TestedExtension {
    public final val applicationVariants: org.gradle.api.DomainObjectSet<com.android.build.gradle.api.ApplicationVariant> /* compiled code */
}

//libraryVariants
public open class LibraryExtension public constructor(dslServices: com.android.build.gradle.internal.services.DslServices, globalScope: com.android.build.gradle.internal.scope.GlobalScope, buildOutputs: org.gradle.api.NamedDomainObjectContainer<com.android.build.gradle.api.BaseVariantOutput>, sourceSetManager: com.android.build.gradle.internal.dependency.SourceSetManager, extraModelInfo: com.android.build.gradle.internal.ExtraModelInfo, publicExtensionImpl: com.android.build.gradle.internal.dsl.LibraryExtensionImpl) : com.android.build.gradle.TestedExtension, com.android.build.gradle.internal.dsl.InternalLibraryExtension {
    public final val libraryVariants: org.gradle.api.internal.DefaultDomainObjectSet<com.android.build.gradle.api.LibraryVariant> /* compiled code */
}

//testVariants
public abstract class TestedExtension public constructor(dslServices: com.android.build.gradle.internal.services.DslServices, globalScope: com.android.build.gradle.internal.scope.GlobalScope, buildOutputs: org.gradle.api.NamedDomainObjectContainer<com.android.build.gradle.api.BaseVariantOutput>, sourceSetManager: com.android.build.gradle.internal.dependency.SourceSetManager, extraModelInfo: com.android.build.gradle.internal.ExtraModelInfo, isBaseModule: kotlin.Boolean) : com.android.build.gradle.BaseExtension, com.android.build.gradle.TestedAndroidConfig, com.android.build.api.dsl.TestedExtension {
    public open val testVariants: org.gradle.api.DomainObjectSet<com.android.build.gradle.api.TestVariant> /* compiled code */
}
Copy the code

By checking the source code, it is found that the types of these three attributes are collections, which can be traversed by the all method. In addition, all these variants have a outputs collection, and each variant has at least one output, which needs to traverse the outputs again

1. Modify the file name of the generated APK package

android {
    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            if(output.outputFile ! =null && output.outputFile.name.endsWith(".apk") && variant.buildType.name == "release") {
                output.outputFileName = "xxx-v${variant.flavorName}-release.apk"}}}}Copy the code

2. Modify the AAR file name generated by the package

android {
    libraryVariants.all { variant ->
        variant.outputs.all { output ->
            if(output.outputFile ! =null && output.outputFile.name.endsWith(".aar") && variant.buildType.name == "release") {
                output.outputFileName = "xxx-v${variant.flavorName}-release.aar"}}}}Copy the code

5. Break the limit of 65535 method

Prior to Android 5.0, a build error was encountered when the number of methods contained in an application and the library it referenced exceeded 65536

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
Copy the code

The problem is that Dalvik restricts each APK to one classes.dex bytecode file, and the total number of referenced methods in a single dex file is 65536, including Android framework methods, library methods, and methods in your own code.

With Android 5.0 as the limit, see how to handle breaking the 65535 method limit

1. Pre-android 5.0

Compile the following in the build.gradle file of the App Module

android {
    defaultConfig {
        multiDexEnabled true
    }
}

dependencies {
  implementation "Androidx. Multidex: multidex: 2.0.1."
}
Copy the code

You need to customize the Application to initialize MultiDex

public class MyApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
     super.attachBaseContext(base);
     MultiDex.install(this); }}Copy the code

2. After Android 5.0

No processing is required, because Android 5.0 runs with ART, which itself supports loading multiple DEX files from APK files. ART performs precompilation at application installation, scanning classESn. DEX files and compiling them into a single.oat file. For Android devices to execute.

Custom BuildConfig

BuildConfig is generated by the Android Gradle build script after compilation. All constants are declared and can be used globally directly, such as buildconfig.debug, new project, open view, etc

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.example.androidconfig";
  public static final String BUILD_TYPE = "debug";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}
Copy the code

You can also add constants to BuildConfig by customizations so that they can be used globally, and Android Gradle provides a function to do this

public open fun buildConfigField(type: kotlin.String, name: kotlin.String, value: kotlin.String): kotlin.Unit { /* compiled code */ }
Copy the code

The first parameter type is the type of the field to be generated, the second parameter name is the name of the constant to be generated, and the third parameter value is the constant value of the field to be generated

At this time came a demand, requiring a page in the debug package load Baidu web page, in the release package load Sina web page. Thus, code the app Module’s build.gradle

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            buildConfigField "String"."URL".'"https://www.baidu.com"'
        }
        debug {
            buildConfigField "String"."URL".'"https://www.sina.com.cn/"'}}}Copy the code

After writing, you need to Rebuild the Project to generate a BuildConfig file

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.example.androidcustomconfig";
  public static final String BUILD_TYPE = "debug";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
  // Field from build type: debug
  public static final String URL = "https://www.sina.com.cn/";
}
Copy the code

And then on that page, just write the address as buildConfig.url, which will automatically replace the different urls as you type different packages

7. Add custom resources dynamically

String resources are usually defined as XML in the Res/Values folder, but can also be defined as Gradle, such as buildConfigField, Android Gradle also provides a function resValue to implement this. Thus, code the app Module’s build.gradle

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            resValue "string"."name"."Android Config"
        }
        debug {
            resValue "string"."name"."Android Config (debug)"}}}Copy the code

After writing, need to Rebuild Project to generate, build\generated\res\resValues\debug\values\gradleResValues. XML this file generates Gradle definition string


      
<resources>

    <! -- Automatically generated file. DO NOT MODIFY -->

    <! -- Value from build type: debug -->
    <string name="appName" translatable="false">Android Config (debug)</string>

</resources>
Copy the code

You can then use it as you would a string defined in XML in the RES/Values folder

DEX option

Java source code is compiled into Class bytecode, and then optimized by dx command to DEX file executable by Android virtual machine when packaged as Apk, in order to run faster, but sometimes prompt out of memory error. You can handle this by modifying the following dexOptions

android {
    dexOptions {
        // Whether to enable the dx delta mode
        incremental false
        // The maximum heap allocated when calling the dx command
        javaMaxHeapSize "4g"
        // Whether to pre-execute the dex libraries project. If dex libraries is enabled, it greatly improves the speed of incremental builds, but also affects the speed of clean builds
        preDexLibraries = false
        // The number of threads used to run the dx command. The appropriate number can improve dx efficiency
        threadCount 2}}Copy the code

Layout subcontract

When there are too many layouts, if they are put under res/ Layout, it is not easy to manage. At this time, subcontracting is needed, similar to code subcontracting

android {
    sourceSets {
        main {
            res.srcDirs = [
                    'src/main/res'.'src/main/res/reLayout/welcome'.'src/main/res/reLayout/home']}}}Copy the code

As shown in figure

Inject build variables into the list

During the construction process, you need to dynamically modify some content in the AndroidManifest file, such as the appId of an SDK needs to be declared in the AndroidManifest, but in some cases, such as the appId of the test package and the production package need to be different, separate statistics.

Android Gradle provides a function for manifestplaceholder that inserts a property value into a manifest file. First define the BUGLY_APPID attribute name in the AndroidManifest file

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidconfig">

    <application>
        <meta-data
            android:name="BUGLY_APPID"
            android:value="${APP_ID}" />
    </application>

</manifest>
Copy the code

Set property values in build.gradle of the App Module

android {
    buildTypes {
        release {
            manifestPlaceholders = [APP_ID: "12345"]
        }
        debug {
            manifestPlaceholders = [APP_ID: "67890"]}}}Copy the code

At this point, different APP_ID can be inserted when different packages are typed

Configure build variants

Each build variant in buildTypes represents a different version of the application that can be built, and can be customized for different needs. For example, if you want to build two versions of your application, a free version with limited content and a paid version with more content.

The build.gradle configuration of the App Module is as follows

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        free {
            applicationIdSuffix ".free"
        }
        vip {
            applicationIdSuffix ".vip"}}}Copy the code

ApplicationIdSuffix said behind the default applicationId add a suffix, such as the default configuration of defaultConfig applicationId for com. Example. Androidconfig, Then build a free package name is com. Example. Androidconfig. Free, so that it can be installed at the same time the mobile phone.

In such cases, it is recommended to write this in the manifest file where the package name is used

<intent-filter>
    <action android:name="${applicationId}.TRANSMOGRIFY" />
</intent-filter>
Copy the code

After this configuration, you can see several additional build variants when it comes time to package

Xii. Configuration of product variants

Product variants are similar to build types in that they support the same attributes as defaultConfig, because defaultConfig itself is a ProductFlavor class. All variants must specify a variety dimension, a product variety group, or an error will be reported

 Error:All flavors must now belong to a named flavor dimension.
 The flavor 'flavor_name' is not assigned to a flavor dimension.
Copy the code

In response to the requirement to “build two versions of your app, a free version with limited content and a paid version with more content”, a variant dimension called Version was created in build.gradle of the App Module and the free and VIP product variants were added

android {
    flavorDimensions "version"
    productFlavors {
        free {
            dimension "version"
            applicationIdSuffix ".free"
            versionNameSuffix "-free"
        }
        vip {
            dimension "version"
            applicationIdSuffix ".vip"
            versionNameSuffix "-vip"}}}Copy the code

When it comes to packaging, you can see several more product variants

Xiii. Application signature

The signature marks the uniqueness of the APP. If the APP is maliciously tampered with, the signature changes, which will result in the failure to upgrade and install. To a certain extent, our APP is protected

android {
    // All signature information configuration block. Multiple signatures can be configured
    signingConfigs {
        // Configure a single block of signature information
        release {
            storeFile file('xxx.jks')     // Path of the signature certificate file
            storePassword 'xxx'         // Sign the certificate file password
            keyAlias 'xxx'                    // Sign the key alias of the certificate
            keyPassword 'xxx'           // Sign the key password in the certificate
        }
        / / the signature of the debug mode is configured by default, the signature file path generally located in the $HOME /. Android/debug keystore
        // You can also customize the signature configuration in debug mode
        debug {
            storeFile file('xxx.jks')
            storePassword 'xxx'
            keyAlias 'xxx'
            keyPassword 'xxx'
        }
    }

    buildTypes {
        release {
            // Type the release package using the release block signature configuration
            signingConfig signingConfigs.release 
        }
        debug{
           // Type the debug package to use the signature configuration of the debug block
            signingConfig signingConfigs.debug
        }
    }
}
Copy the code

It is common practice to put the signature file in the project, but this is not a good security practice. Instead, the signature file should be put on the packaging server and read by the environment variable

android {
    signingConfigs {
        def theStoreFile = System.getenv("STORE_FILE")
        def theStorePassword = System.getenv("STORE_PASSWORD")
        def theKeyAlias = System.getenv("KEY_ALIAS")
        def theKeyPassword = System.getenv("KEY_PASSWORD")
        // Copy the Default Android debug signature file to the project root directory
        // If the official signature cannot be obtained, use the debug signature file
        if(! theStoreFile || ! theStorePassword || ! theKeyAlias || ! theKeyPassword){ theStoreFile ="debug.keystore"
            theStorePassword = "android"
            theKeyAlias = "androiddebugkey"
            theKeyPassword = "android"
        }
        release {
            storeFile file(theStoreFile)
            storePassword theStorePassword
            keyAlias theKeyAlias
            keyPassword theKeyPassword
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release  
        }
    }
}
Copy the code

Automatically clean up unused resources

As a project gets larger, it inevitably generates resources that are no longer used, which, if not cleaned up, will increase the size of the APK package and the build time. While it’s possible to manually clean it up on a regular basis, it depends on the developer’s awareness and familiarity with the logic of the code, and it’s not possible to handle this if you introduce third-party libraries that also have unused resources. So Android Gradle provides a way to automatically clean up unused resources when they are packaged

public open var isShrinkResources: kotlin.Boolean
Copy the code

It detects all resources, both your project’s own and those of referenced third parties, before packaging them into apK, and automatically processes unused resources. It is not enabled by default, but can be enabled in the App Module, and this method works only when used in conjunction with code obfuscation.

android {
    buildTypes {
        release {
            // Enable resource reduction
            shrinkResources true
            // Turn on code obfuscation
            minifyEnabled true}}}Copy the code

The “automatically clean up unused resources” feature is good, but it can also cause errors. For example, when using reflection to reference resource files, Android Gradle will not be able to tell.

If you want to keep or discard resources, create an XML file in your project that contains the resources tag, then specify each resource to keep in the Tools :keep property and each resource to discard in the Tools :discard property. Both properties accept a comma-separated list of resource names, and asterisk characters can be used as wildcards.

Such as res/raw/keep. XML


      
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />
Copy the code

Use shared libraries

Android packages (such as Android.view, Android.Content, etc.) are located in the default libraries that all apps automatically associate with, and all apps can be used directly, with the system linking them automatically. Some libraries, such as com.google.android.maps, are independent and will not be automatically linked to the system. They are created and used separately. These libraries are called shared libraries.

For example, in the AndroidManifest setting

<manifest>
    <application>
        <uses-library
            android:name="com.google.android.maps"
            android:required="true" />
    </application>
</manifest>
Copy the code

Once declared, the system checks for the shared library when installing the generated APK package because Android: Required =”true” is set. If not, the application cannot be installed.

There is an optional optional libraries, they located in xx/platforms/android – optional directory, generally to compatible with the old version of the API, such as org. Apache. HTTP. Legacy, this is a HttpCLient library, As of API 23, the Android SDK no longer includes the HttpClient library, and you must set up this optional library if you want to use it.

Set in the App Module

android {
    useLibrary "org.apache.http.legacy"
}
Copy the code

It’s a good idea to also use uses-library in the AndroidManifest in case something goes wrong.