Detailed introduction of building Android pre and post tasks with Gradle

Keywords: Gradle

Detailed introduction of building Android pre and post tasks with Gradle

preface

"Building Android pre and post tasks" refers to some operations before and after Android packages. For example, the version number is automatically modified before packaging, and reinforced after successful packaging.

The last article briefly introduced the general idea of transforming Android construction from python / shell and other scripting languages to gradle. In this article, I will describe the specific operation process in detail and some problems encountered by the author at the end. I hope readers can read it later

  • Understand the code architecture of building Android applications using gradle
  • With the help of relevant documents, you can implement relevant processing steps yourself. (add pre and post tasks, etc.)

Introduction – use python as an introduction to introduce the basic idea of gradle

If you directly describe the code architecture using gradle and show the code, it may confuse people. Therefore, from the perspective of my own easy to understand, I will introduce the easy to understand ideas of this change. With my understanding ability, I believe readers can see the gains and losses of doing so.

Assumptions:

We already have a python script to package Android, which has the following features:

  • It has existed for a long time. It has existed since the project three years ago, followed by a series of repairs.
  • Complex functions, including but not limited to:
    • 1) Before packaging: modify the version number according to the configuration file; Modify the gradle compilation parameters according to an eigenvalue of this package (even because the modification in different periods may use two different methods to modify different parameters - but it doesn't need to be so complicated for simple consideration here)
    • 2) After packing: reinforce according to packing configuration; And play multiple channel packages; Upload the package to a specified storage platform.
    • The functions mentioned above are different according to the test environment or online environment configured during packaging (for example, the test environment is uploaded to one location, while the online environment is another.)
  • Because there is no special person to maintain it, it will be changed every time new functions are needed in packaging, resulting in poor code architecture. It's troublesome to modify.
  • There are two different applications, ProductA & productb, which use this packaging script, so that there are a lot of if(currentProduct == productA) {} logic in the packaging script.

Then we received the task of reconstituting the packaged script into a form that is easy to understand and easy to expand.

🤔, So what should we do? In my humble opinion, the following ideas are a better scheme:

  • The implementation codes of different functions are gathered together and made into independent Python modules to provide a single function. For example, you can
    • Organize the reinforcement code into a Jiagu module and expose a Jiagu (srcapk, dstapk, Jiagu method) # parameter to represent the reinforcement source / location of generating apk / reinforcement method respectively.
    • Organize the code generating the channel package into a channelModule, and expose a buildchannels (srcapk, channels) # parameter to indicate the channel source apk / which channel information to generate.
  • Two different applications use different scripts and are not mixed together. In the script, the functions provided by the above modules are assembled to realize independent construction.

In this way, we split the previous python script into two parts: one is the encapsulated function module; Part is the script content. In the script content, it is actually some process design based on encapsulated function modules.

The encapsulation function module here can also be implemented using separate functions.

In this way, we get the following new Python script:

  • Encapsulated functional modules
    • Modify version number module
    • Reinforcement module
    • Channel package module
    • Upload module
  • Actual script module
    • Build script for productA
    • Build script for productB

The reconstructed python script is great, but it has one disadvantage: it is python language. For Android development, its readability and maintainability are not so high. It would be very gratifying if it could be changed to kotlin.

Then let's modify the reconstructed python script into the form of gradle script. Why do you need to modify it? Because we want to change python to a familiar kotlin for better understanding and modification. The change is very simple, just change the encapsulated function module to gradle task, and the actual script module to gradle script. In order to prevent some readers from not understanding the basic knowledge of gradle task, the following briefly introduces the gradle task understood by the writer.

A brief introduction to gradle task

In this section, you need to know how to use buildSrc

How should gradle task be declared?

Here is a simple method. We create a new directory called buildSrc under the root directory of the Android project. The usage of this directory is the same as that of ordinary Android modules. You can add dependent libraries such as okhttp. Different from ordinary Android modules, the source code in this directory will be compiled before the gradle script is built, that is, in the gradle script, you can directly access these classes and their data (such as constants).

As for the specific usage of buildSrc and how to set the dependency Library in the directory. I won't elaborate here. After all, this is not the focus of this article. If readers want to know, it is recommended to search relevant articles on the Internet. There are many simple and easy to understand articles. Search for the keyword "how to use android buildSrc".

Hard to read official document address: How to declare a task type using kotlin . If you find it difficult to understand, it's also normal. You don't need to force reading. You can check some other articles or leave a message to ask the author

If some readers can't use buildSrc, it's recommended to learn it first so that they can know how to operate later. However, this does not affect the general process. Here, you only need to know that the classes in buildSrc can be accessed in the gradle script. The gradle task can be put into the buildSrc module and used in the script like accessing third-party library code.

The expression form of gradle task has always been very scary, either Android gradle plugin or third-party plugin. These are too difficult to understand, and the author doesn't know. So let's briefly understand here: gradle task is actually an executable function (ignore all its doFirst / doLast attributes first).

In the above reconstructed python script, the encapsulated function modules will be transferred to the form of gradle task. The meaning of this is also obvious: it means that these tasks are used to provide functions, just like function calls.

However, we have written the task, but we have only written the function. It cannot take effect without calling. Next, let's talk about how to call these tasks to perform various operations.

Reference to task in gradle script

In gradle, various tasks can build dependencies on each other. We have declared many functional tasks above, which are used in gradle scripts. As for the use of gradle, it's hard to say. I can't describe it in detail, so I listed the demo code directly below. Please check it. If you have any doubts or corrections, you are welcome to put forward them 👏🏻.

Modification summary

The demo code will be listed below. First, orally explain what changes are needed to make readers more understand. (for example, it is not rigorous enough, readers forgive me)

  • Add an operation to be performed before packaging. Take the modified version number as an example
  • Add an operation to be performed after packaging. Take the reinforcement after packaging as an example
  • Packaging step design script.
  • Changes to the gradle script to help the above three run (including two, one is the dependency configuration of buildSrc; the other is to introduce the above steps to design the script)

The changes are as follows

new file:   a.build.gradle.kts
modified:   app/build.gradle
new file:   buildSrc/build.gradle
new file:   buildSrc/src/main/java/postassemble/Jiagu.kt
new file:   buildSrc/src/main/java/preassemble/ModifyVersion.kt

demo code

// buildSrc/src/main/java/postassemble/ModifyVersion.kt
// Declare a task to describe the operation performed before packaging
package preassemble

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile

// The example here is only to illustrate the capability of outputFile.
// Write the given [newVersion] to [configFile], and create a new one if [configFile] does not exist.
abstract class ModifyVersion : DefaultTask() {

    @org.gradle.api.tasks.Input
    lateinit var newVersion: String

    @OutputFile
    lateinit var configFile: java.io.File

    @org.gradle.api.tasks.TaskAction
    fun action() {
        println("${configFile} The version number in has been changed to ${newVersion}")
    }
}
// buildSrc/src/main/java/postassemble/Jiagu.kt
// Declare reinforcement task type
package postassemble

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile

// This task consolidates the srcFile and outputs it to outputFile
// The reinforcement method used is specified by jiaguMethod
abstract class Jiagu : DefaultTask() {

    @InputFile
    lateinit var srcFile: java.io.File

    @OutputFile
    lateinit  var outputFile: java.io.File

    @Input
    lateinit  var jiaguMethod: String

    @org.gradle.api.tasks.TaskAction
    fun jiagu() {
        println("${srcFile} adopt ${jiaguMethod} After reinforcement, the reinforced document is located in ${outputFile}")
    }
}
// buildSrc/build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url 'https://maven.oschina.net/content/groups/public/' }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}

plugins {
    id "org.jetbrains.kotlin.jvm" version "1.5.0"
}

allprojects {
    repositories {
        google()
        maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url 'https://maven.oschina.net/content/groups/public/' }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}
dependencies {
    // You can add third-party libraries such as okhttp / glide here, and then use the source code in src /.
}

// a.build.gradle.kts
// Note: this script file defines tasks and the relationship between tasks. Need to introduce.
import preassemble.ModifyVersion
import postassemble.Jiagu

project.afterEvaluate {
    val rootDir = project.rootDir
    // Step description: create a new task named "modifyVersion"
    val modifyVersion = project.tasks.register<ModifyVersion>("modifyVersion") {
        newVersion = "1.0"
        configFile = File(rootDir, "config")
    }.get()
    // Step description: get packaging task
    val assembleTask = tasks.findByName("assembleRelease") ?: throw IllegalStateException("Package task not found")
    // Step description: create a new task named "jiagu"
    val jiagu = tasks.register<Jiagu>("jiagu") {
        // Because my demo project is not configured with signature, it is typed with - unsigned, just for example
        // This should be the source apk of the reinforcement task input.
        srcFile = java.io.File("build/outputs/apk/release/app-release-unsigned.apk")
        outputFile = File("app/build/outputs/apk/release/app-release-jiagu.apk")
        jiaguMethod = "cccc"
    }.get()

    // Step description: set dependencies between tasks
    assembleTask.dependsOn(modifyVersion)
    jiagu.dependsOn(assembleTask)
}

Unreasonable situation encountered (PIT)

  • *The. gradle.kts file has no syntax highlighting.

    1. Open "Preferences" - > "Language & frameworks" - > "Kotlin" - > "Kotlin Scripting"

    2. Add the specified. gradle.kts file to "Gradle Kotlin DSL Scripts" below the right.

    After adding, Android Studio will highlight syntax and automatically prompt.

  • In the gradle.kts file, I have a task to modify the properties of a build.gradle before executing the assemblyrelease, but the apk typed out has not taken effect. Why?

    This is because modifying the gradle file after calling gradle to execute the task does not take effect.

    For example, in the above demo, if. / gradlew: app: Jiagu is directly executed, although assemblyrelease is an intermediate task, its execution is also subject to the gradle script content when calling. / gradlew: app: Jiagu.

    So in gradle.kts, if you want to use task A to modify A build.gradle file and then package it. You must execute the command first. That is, you need to:

    1. . / gradlew: app: a (this task modifies app/build.gradle, for example, replacing the app_name attribute declared therein)
    2. . / gradlew: app: Jiagu (for example, it represents the final task. Reinforcement is also the ultimate goal here)
  • Why can't I find a task using tasks. FindByName ("assemblyrelease") in my script?

    • It may be because the task is not obtained in project.afterEvaluate {}. The assemblyrelease task will not exist until the android plug-in is executed, so the new task created by the plug-in will not exist until the project evaluate is completed.
    • It may be that the app module does not have an assemblyrelease task. For example, when flavor is set, it needs to be modified according to the specific task name.
  • Dependencies (such as okhttp / gson) are added in buildSrc, but the code in build/src cannot automatically import the class of the third-party library and cannot find the class. Why?

    I think it's an AS BUG. It actually exists, but the automatic import fails at this time.

    My solution is to manually view the location of a class and then manually enter import. For example, first check that the package name of OkHttpClient is okhttp3, and then enter import okhttp3.OkHttpClient in the header of the file to be used.

Posted by reeksy on Tue, 30 Nov 2021 19:42:32 -0800