1 background

At present, with the popularity of Android modular, plug-in and component-based App architecture design, each plug-in or component in the project is usually deployed in different source repositories, and the upgrade and maintenance cost of the dependent library version referenced by these repositories becomes higher. When the version number of a dependent library is upgraded, the version number in the shell project and related plug-in or component source repository must be changed synchronously, resulting in high maintenance costs. In order to solve this problem, this article will step by step introduce a unified version management scheme for Android multiple source repositories.

Unified management of dependent library versions of single source repository

Before introducing the unified version management scheme of dependent libraries in multiple warehouses, the unified version management configuration method of multiple modules in a single warehouse is briefly introduced here.

First, create a config. Gradle configuration file that depends on the library version in the Project root directory, with contents similar to the following:

Ext {depsVersion = [supportV7_androidx: '1.1.0', design_Androidx: '1.1.0', recyclerView_Androidx: '1.1.0', kotlin: '1.3.72', uilib: '2.38.0'] depsLibs = [design_androidx: "com.google.android.material:material:${depsVersion.design_androidx}", recyclerview_androidx: "androidx.recyclerview:recyclerview:${depsVersion.recyclerview_androidx}", supportV7_androidx: "androidx.appcompat:appcompat:${depsVersion.supportV7_androidx}", gson: "com.google.code.gson:gson:${depsVersion.gson}", kotlin_gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${depsVersion.kotlin}", kotlin_android_extensions: "org.jetbrains.kotlin:kotlin-android-extensions:${depsVersion.kotlin}", kotlin_stdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${depsVersion.kotlin}", uilib: "com.bk:uilib:${depsVersion.uilib}@aar" ] }Copy the code

The depsVersion tag declares the version number of each dependent library, and the depsLibs tag declares the constant of each dependent library.

Then, reference this configuration file in the build.gradle file at the top of the Project by applying from: “config.gradle” :

buildscript {
    apply from: "config.gradle"

    repositories {
        maven {
            jcenter()
        }
     ....
Copy the code

Finally, we can uniformly rely on the dependency libraries defined in the config.gradle configuration file in each child module in the following way:

dependencies {
  implementation rootProject.ext.depsLibs.supportV7_androidx
  implementation rootProject.ext.depsLibs.recyclerview_androidx
  implementation rootProject.ext.depsLibs.kotlin_stdlib
  implementation rootProject.ext.depsLibs.gson
  ...
Copy the code

In the future, if a dependent library needs to be upgraded, you only need to modify the config.gradle file as a whole, rather than on a per-module basis. This implementation is perfect within a single source repository.

3. Problems in the maintenance of dependent library version of multi-source repository

However, with the increase of project complexity, framework design such as plug-in, modularization and componentization usually divides App engineering into multiple warehouses. The following figure is a common architectural design:

Each component or plug-in library, as well as the base library, is a separate source repository that references many dependent libraries along with the shell project. Each repository maintains a copy of config.gradle, and depending on the version number of the library to update can be a headache. Every time you upgrade a library, you need to modify the config.gradle configuration in the shell project, the source repository for each plug-in, component, and base library.

The downside of this is:

  • Version number maintenance is complex, and each upgrade involves modifying configuration files in multiple source repositories

  • Manual modification is prone to error, resulting in the inconsistency of the dependent library versions between the repositories, which leads to compilation problems, functional exceptions and crash problems, such as NoSuchMethodError and VerifyError

4 How to Unify the version configuration files of multiple repositories

Shell project is a source repository that truly integrates compiled dependencies. All we need to do is hope that all other repositories can reference shell project config. Gradle configuration. How do I converge the config.gradle dependency library configuration files from multiple repositories into one file?

Gradle file in the shell project. For example, if all the repositories are in the same directory, they can be referenced as follows:

buildscript { apply from: ".. /ke_main_project/config.gradle" ....Copy the code

This does allow all repositories to share the same config.gradle configuration, but there is a serious problem with this local dependency. When the shell project and other repositories are not on the same iteration branch of the version, there will be compilation failure and the dependency version number does not match. For example, if the shell project is currently in the 1.0 development branch and the current branch of another repository is in the 2.0 development branch, if the config.gradle configuration file of the shell project on the 2.0 branch is updated, the other repository may not reference the new dependency library or reference the wrong old dependency library. In addition, there is the limitation that multiple warehouses need to be in the same directory, and this local dependency approach is (1) insecure and (2) inflexible. We then moved to consider a remote dependency approach.

It is important to note that the “apply from” parameter of Gradle scripts can not only be the scripts of the local file system, but can also specify the script application in a remote location, that is, the remote script file of the HTTP URL. Can we put the config. Gradle configuration file in the cloud? Similar to the design shown in the following figure, other warehouses of non-shell projects unify the remote dependency library version profile of the shell project.

The answer is yes, remote dependencies do solve the problem of local dependencies. After further investigation, we found that the API provided by many common code hosting platforms supports network request to obtain a certain file within the code, and with git’s version management capability, we can specify the configuration file of a specific branch of the remote end, without the need to build an additional server to store the configuration file.

Implementation of unified management scheme of dependent library version of more than 5 source repositories

Next, we will detail two common code hosting platforms, GitLab and Gerrit, to achieve the unified configuration of the dependent library versions of multiple repositories. As for the implementation of other code hosting platforms is similar, this article will not repeat.

5.1 gitlab

First, you need to apply for Personal Access Tokens to Access the interface. The application path is in the Personal Settings page of GITLab:

Gitlab access interface can be the reference of the original documents link docs.gitlab.com/ee/api/repo…

GET /projects/:id/repository/files/:file_path/raw
Copy the code

Parameter Description:

  • Id parameter: Is the ID of the project

  • File_path (required) – is the relative path of the files in the Project and requires URLEncode for the files

  • Ref (must) -branch, tag or commit name, URLEncode required

The project ID is a numeric type. You need to confirm the project ID of the lower shell project through the request query interface. You can use the curl command to query the project ID quickly.

Curl --header "PRIVATE TOKEN: {your access TOKEN}" "{your gitlab domain}/ API /v4/search? Scope =projects&search={shell main project name}"Copy the code

The return result is JSON, for example, the following id field is the project ID.

[ { "id": 12345, "description": "Nobis sed ipsam vero quod cupiditate veritatis hic.", "name": "Flight", "name_with_namespace": "Twitter / Flight", "path": "flight", "path_with_namespace": "Twitter/flight", "created_at" : "the 2017-09-05 T07:58:01. 621 z", "default_branch" : "master", "tag_list":[], "ssh_url_to_repo": "ssh://jarka@localhost:2222/twitter/flight.git", "http_url_to_repo": "http://localhost:3000/twitter/flight.git", "web_url": "http://localhost:3000/twitter/flight", "avatar_url": Null, "STAR_count ": 0," forkS_count ": 0, "last_activitY_at ":" 2018-01-3t9:56:30.902z "}]Copy the code

There are two ways to implement this in build.gradle scripts at the root of other repositories in non-shell projects.

First, the token needs to be used as the header parameter request interface. First, download the shell project configuration file to the local, apply from reference, delete the file:

Def HOST_BRANCH = 'feature_1.0.0' def HOST_PROJECT_ID = '{shell project id}' def REF = URLEncoder.encode("$HOST_BRANCH", 'UTF-8') URL configFileUrl = new URL( "Gitlab domain name} {you/API/v4 / projects / $HOST_PROJECT_ID/repository/files/config. Gradle/raw? ref=$REF") URLConnection urlConnection = configFileUrl.openConnection() urlConnection.setRequestProperty("PRIVATE-TOKEN", "{your access token}") urlConnection. InputStream. WithStream {file (" config. Gradle ") bytes = it. The bytes} the apply the from: "config.gradle" delete "config.gradle" ...Copy the code

The second method, which is simpler to implement, is recommended. The GitLab interface documentation does not mention that tokens can also be used as query parameter request interfaces, so we can directly apply from file links to implement remote dependencies.

Def HOST_BRANCH = 'feature_1.0.0' def HOST_PROJECT_ID = '{shell project id}' def REF = URLEncoder.encode("$HOST_BRANCH", 'UTF-8') apply from: "Gitlab domain name} {you/API/v4 / projects / $HOST_PROJECT_ID/repository/files/config. Gradle/raw? Ref =$REF&private_token={access token}"...Copy the code

5.2 gerrit

Gerrit warehouse file interface links refer gerrit-review.googlesource.com/Documentati…

'GET /projects/{project-name}/branches/{branch-id}/files/{file-id}/content'
Copy the code

Parameter Description:

  • Project name: – the project name
  • Branch – id: branch name
  • File-id: indicates the file path

All three parameters need to be URLEncode compiled.

Similarly, gerrit’s interface request also requires token, which can be generated in gerrit’s personal Settings page, as shown below:

Copy the generated HTTP password. Do not click Generate again, otherwise the previous password will be invalid.

Finally, the build.gradle script for other non-shell repositories is implemented as follows:

Def HOST_BRANCH = 'feature_1.0.0' def REF = URLEncoder. Encode ("$HOST_BRANCH", 'utf-8 ') def MAIN_PROJECT_NAME = URLEncoder. Encode ("{shell Project Name}", 'utF-8 ') def PWD = "{your gerrrit username}:{your generated HTTP password}". GetBytes (" utF-8 ") String token = Base64.getEncoder().encodeToString(pwd) URL configFileUrl = new URL( "Gerrit domain name} {you/a/projects / $MAIN_PROJECT_NAME/branches / $REF/files/config. Gradle/content") URLConnection URLConnection = configFileUrl.openConnection() urlConnection.setRequestProperty("Authorization", "Basic " + token) urlConnection.inputStream.withStream { file("config.gradle").bytes = Base64.getDecoder().decode(it.bytes) } apply from: "config.gradle" delete "config.gradle" ...Copy the code

In addition, the HOST_BRANCH variable needs to be modified for each iteration branch, so it is obviously cumbersome to specify the dependency library version profile for the corresponding branch of the shell project. It is recommended that the same branch name be used for each iteration of all repositories, so that the HOST_BRANCH variable can dynamically obtain the branch name of the current repository using git commands without changing it every time:

def HOST_BRANCH = 'git symbolic-ref --short -q HEAD'.execute().text.trim()
Copy the code

Through the above implementation, each component library or base library in the compilation, will be unified remote dependency shell project dependency version configuration file, when involves a dependency library version upgrade, only need to modify a configuration file in the shell project, perfect implementation of multiple source repository dependency library version unified management.

6 summary

With the help of the API interface and version management capability of the code hosting platform, this paper implements a unified version management scheme for the dependent libraries of Android multi-source repositories, which converges the version configuration files of the dependent libraries of multiple repositories into one, which not only reduces the maintenance cost, but also improves the stability of App. Hope to bring some help to everyone’s project development.