Welcome to follow the official wechat account: FSA Full stack action 👋

I haven’t written a blog for a long time, and I suddenly want to share the summary of the technology and multi-channel related understanding used in the project during this period

First, new channels

Using AndroidStudio and Gradle, you can export multiple packages conveniently. You only need to configure productFlavors in the Build. gradle section of the App Module. Gradle has a US version and a free version.

android {
    productFlavors {
        china { / / the Chinese version
        }
        america { / / the United States
        }
        free { / / the free version}}}Copy the code

Once this is all done, there will be a variety of channels to choose from in the Build Variants TAB of Android Studio. When we want to run an APP directly from a channel using AS, we will need to select the Variants in the Build Variants TAB and then click the “Run” button to run the project.

ProductFlavors can be configured with applicationId, versionCode, versionName, Icon, and application name.

free {
	applicationId 'com.lqr.demo.free'
	versionCode 32
	versionName '1.3.2'
	manifestPlaceholders = [
			app_icon: "@drawable/ic_launcher",
			app_name: "Vegetable Chicken [Free Version]"]},Copy the code

Note: the packageName configured here is applicationId, not packageName in the manifest file. ApplicationId and packageName are different. You can’t have two apps with the same package name installed on an Android device at the same time. The difference between applicationId and packageName can be found in applicationId versus packageName

This multi-channel configuration can be useful if the project requires the coexistence of different channels, or if there are custom requirements for version numbers, ICONS, application names, and so on. When you put app_icon, app_name, and manifestplaceholder in androidManifest.xml, it modifies the placeholder in your androidManifest.xml. You also need to make some minor changes to the manifest file (adding some placeholders), such as:

<application
    xmlns:tools="http://schemas.android.com/tools"
    android:icon="${app_icon}"
    android:label="${app_name}"
    android:theme="@style/AppTheme"
    android:largeHeap="true"
    tools:replace="android:label">.</application>
Copy the code

Second, generate channel variables

After adding channels, we can configure these channels more, assuming that the project code needs to assign different data according to different channels. Of course, you can choose to set static constants in Java code by judging the current channel name and cooperating with switch, but in fact, it is not so complicated. Gradle or config.properties configuration files will help you find some static data.

// Multi-channel related Settings
applicationVariants.all { variant ->
    buildConfigField("String"."PROUDCT"."\"newapp\"")
    buildConfigField("String[]"."DNSS".{", "http://119.29.29.29\", and "http://8.8.8.8\", \ "http://114.114.114.114\"}")
    if (variant.flavorName == 'china') {
        buildConfigField("String"."DNS"."\" http://119.29.29.29\ "")}else if (variant.flavorName == 'america') {
        buildConfigField("String"."DNS"."\" http://8.8.8.8\ "")}else if (variant.flavorName == 'free') {
        buildConfigField("String"."DNS"."\" http://114.114.114.114\ "")}}Copy the code

With buildConfigField() provided with gradle, AndroidStudio changes the configuration to a set of static constants in buildconfig.java at script initialization based on the currently selected variant:

When I switch to other variants, the DNS in BuildConfig changes along with it, so that we don’t need to determine the current channel name to assign static constants in our project code. BuildConfigField () is used to generate String and String[] constants, but it can also be used to generate other types of constant data if you are interested.

Third, the use of variants

There are variants, so what are variants? It can be understood that variants are a combination of [Build Type] and [Product Flavor], and there are [Build Type] * [Product Flavor] combinations. For example, there are two Build types as follows, and two channels are configured:

Build Type: release debug Product Flavor: China FreeCopy the code

There will eventually be four types of Build Variant:

chinaRelease chinaDebug freeRelease freeDebug
Copy the code

Variants can be useful in complex multi-channel projects, such as resource file merging and code integration. When we use Android Studio for project development, we will store the code files and assets files in the app/ SRC directory. Usually, Java, RES and Assets are stored in the main directory to separate the code files and assets files. You can think of main as the default project directory. This means that the code files and resource files stored under Main are held jointly by all channels.

So what happens when some code files or resource files come out that are channel specific? Since main is common, ideally we don’t want to put these “non-generic” files under main (it’s safe to do this, but it’s very low and increases the size of the APK package). Android Studio does a good job of supporting variations. Create a directory named after the channel to store such channel-specific code files and resource files, such as:

As you can see, when I select the freeDebug variant, the directories under app/ SRC/Free are highlighted, indicating that they are recognized by Android Studio. When running the project, Android Studio will merge all the resource files under Free and main, and consolidate the code files. Similarly, if I had chosen the chinaDebug variant, the app/ SRC/China directory would have been highlighted. Now that you know how to create a variation directory, it’s time to merge resources and code.

1. Resource consolidation

What are the resource files? We can think of it this way:

Resource files = all files under res + androidmanifest.xmlCopy the code

The resource merge function of the variant is a “magic tool”, which can solve many business needs, such as different ICONS displayed in different channels, different application names, etc. Android Studio follows this rule when merging resources from the variants directory and the main directory, assuming that the currently selected variant is freeDebug:

  • A resource that is available in free but not in main is merged directly into the main resource during packaging.
  • If a resource exists in both free and main, the package is mainly free and the resource in free is replaced with the resource in main.

For the above two rules, string. XML is used as an example.

<resources>
    <string name="app_name">Demo</string>
    <string name="app_author">Lin</string>
</resources>
Copy the code

String.xml under free is:

<resources>
    <string name="error_append">An error occurred</string>
    <string name="app_author">Lqr</string>
</resources>
Copy the code

So the string. XML in the apK package is:

<resources>
    <string name="app_name">Demo</string>
    <string name="error_append">An error occurred</string>
    <string name="app_author">Lqr</string>
</resources>
Copy the code

In addition to string merge, there are images (drawable, mipmap), layout (layout), manifest file (Androidmanifest.xml) merge, specific can try yourself. Androidmanifest.xml in the channel directory has the same node attribute as androidmanifest.xml in main, but the attribute value is different. Androidmanifest.xml (androidmanifest.xml) : androidmanifest.xml (androidmanifest.xml) : androidmanifest.xml (androidmanifest.xml) : androidmanifest.xml

Note: The layout file is merged to replace the entire file.

2. Code integration

Code file, as the name implies refers to the Java directory. Java file, why code is called integration, and resources are merged? Because you can’t merge code files, you have to merge them. What do you mean by merge? Suppose the currently selected variant is freeDebug, and there is a Java file named test. Java that exists either in free/ Java or in main/ Java, as in:

Test. Java is recognized by AndroidStudio, but if test. Java is also present under main/ Java, AndroidStudio will report an error:

Code integration is a bit of a headache, because if you refer to a class in main from the channel directory free, it’s fine, but if you refer to a class in free from main, it’s bad, and when you switch to another variant (e.g., chinaDebug), Test. Java is free only. Under chinaDebug, free is not recognized, so main cannot find the corresponding class.

Test.java is normally referenced when selecting the freeDebug variant:

Test.java cannot be found when selecting chinaDebug variant (only test.java under junit is found) :

So, for code integration, we need to think about how to decouple the channel directory from the main directory during development. For example, you can use Arouter to decouple all activities and fragments in the main directory from those in the channel directory, convert class references to string references, and all of them will be managed by Arouter, or handled by reflection, etc. By the way, In my project, I used ARouter to determine whether activities and fragments exist, and related methods to obtain them:

/** * Get the target Delegate (Fragment only) */
public <T extends CommonDelegate> T getTargetDelegate(String path) {
    return (T) ARouter.getInstance().build(path).navigation();
}

/** * Get the target class (support Activity, Fragment) */
publicClass<? > getTargetClass(String path) { Postcard postcard = ARouter.getInstance().build(path); LogisticsCenter.completion(postcard);return postcard.getDestination();
}
Copy the code

3, other

I just mentioned res and Java. What about assets? Which one does it belong to? Unfortunately, although assets are resources, they are not merged, but integrated. That is to say, assets files are processed in the same way as Java files. The same assets files cannot exist in the channel directory and the main directory at the same time. Suppose that China and Free use the same assets, while America uses its own assets, and these assets file names are the same, what should we do in this case? Give each channel a copy of its own assets? This works, but it’s low, for the following reasons:

  1. Poor reusability: It has been said that the resources used by China and free are the same. From the perspective of the whole project, there are two identical assets files in one project. What if I have 10 channels and 9 of them use the same assets?
  2. High maintenance cost: in the development of industry, the demand for change is very common things, product manager will change from time to time under the demand, so, call you also is likely to change assets resource files, if you put a using every channel, so when assets resources need to be changed, you need to replace each channel resources assets directory again. Remember, you have to replace it every time you change it.

The correct solution is to use sourceSets, the use of which is explained in the next section.

Four, sourceSets

Gradle is powerful. SourceSets allows developers to customize project structures such as assets, Java, RES, and more. However, sourceSets does not break the merge rules of variants. SourceSets simply “augments”. Here are some common uses of sourceSets:

sourceSets {
	main {
		manifest.srcFile 'AndroidManifest.xml'
		java.srcDirs = ['src']
		aidl.srcDirs = ['src']
		renderscript.srcDirs = ['src']
		res.srcDirs = ['res']
		assets.srcDirs = ['assets']}}Copy the code

1. Reuse assets

For the problem of multiple channels sharing the same set of assets files, we can handle it in sourceSets as follows:

  1. Store the shared assets in a channel directory, for example, free/ Assets.
  2. Modify the sourceSets rule to specify the Assets directory of China channel as Free/Assets.
sourceSets {
	china {
		sourceSet.assets.srcDirs = ['src/free/assets']}}Copy the code

After this configuration, the next time you need to modify the China and Free Assets files, you only need to replace the free/ Assets files. If you have 20 channels, all using the same set of assets, you will have to write the sourceSets configuration 19 times.

sourceSets {
	china {
		sourceSet.assets.srcDirs = ['src/free/assets']
	}
	a{
		sourceSet.assets.srcDirs = ['src/free/assets']
	}
	b{
		sourceSet.assets.srcDirs = ['src/free/assets']}... }Copy the code

The sourceSets configuration alone is as long as you can imagine in this Gradle file. You might say, how can there be so many channels for a project? Sorry, my company’s business needs have 20+ channels. Gradle is a script that writes logic like code, so the above configuration can be converted to an if-else code fragment:

sourceSets {
	sourceSets.all { sourceSet ->
	// println("sourceSet.name = ${sourceSet.name}")
	if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
		if (sourceSet.name.contains("china") 
				|| sourceSet.name.contains("a")
				|| sourceSet.name.contains("b") | |...). { sourceSet.assets.srcDirs = ['src/free/assets']}}}}Copy the code

Now you might think that this doesn’t seem like a lot of simplification, but once your business gets more complex, it’s a good idea to use code logic to handle configuration like this.

If you are interested, print sourceset.name; The if notation does not necessarily contain (), but it is up to the developer to use some other notation.

2. Modify the main entrance of the program

In addition to modifying assets, Java files, RES resource files, manifest files, etc. can be “expanded” in the same way. For example, the Java code logic shared by different channels can be extracted and stored in a separate directory. It is then added using sourceSets. Here is a personal example of how sourceSets can be used to specify Java and manifest files and perfectly address such “perverse” needs.

1) background

The development of the new APP project has been completed, and now the project needs to be customized and put online. The overall project adopts the architecture of 1 Activity + N Fragment, and this Activity is the main entrance of the program, because our product is the development of SET-top box APP. After the product development is completed, We need to go online to the app store of the box operator (the customer), and then launch the app we developed through the box Recommendation bit (EPG). Therefore, after going online, we need to provide the package name and class name of the app to the customer. Assume that the package name and class name of the new app are as follows:

Package name: com. LQR. Newapp class name: com. LQR. Newapp. MainActivityCopy the code

2) demand

Change the package name and class name of the new app to the same as the old app, because the customer does not want to change ~~ suppose the package name and class name of the old app are as follows:

Package name: com. LQR. Oldapp class name: com. LQR. Oldapp. MainActivityCopy the code

3) problem

Modify the package name is very simple, but change the name of the class entrance is very trouble, if I were in the channel directory. A new com LQR. Oldapp. MainActivity, registered in the manifest file, so, in the packaging, Androidmanifest.xml in the channels directory is merged with androidmanifest.xml in the main directory.

And the main directory AndroidManifest. XML has been registered com. LQR. Newapp. MainActivity, this will lead to final output apk package listing in the file will have 2 entrance class.

Yes, such a product can indeed meet the needs of the bureau when it is delivered. However, once this APP is installed in the box, two entry ICONS may appear on the box Launcher at the same time, which will be another trouble. After all, the app launching process is quite troublesome, so we’d better ensure that the product has only one entry.

4) analysis

Because of the variant of the resource merge rule, as long as there is androidmanifest.xml in both the channel directory and the main directory, the manifest file in the APK package will be merged into two files. Therefore, you cannot register entries in the two manifest files separately. Androidmanifest.xml can be extracted from 2 different entries and stored in other directories. Androidmanifest.xml in main only registers common components.

5) Operation:

A. Remove MainActivity (oldApp)

In the app directory, create a support/directory entry (optional), to hold the entry related functional code and resource files, will com. LQR. Oldapp. MainActivity to support/entry/Java directory.

B. pull away AndroidManifest. XML

In the support directory, create the manifest (name is optional) to store the androidmanifest.xml corresponding to each channel, such as:

Androidmanifest.xml in newapp directory:

<application>
    <activity android:name="com.lqr.newapp.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>

            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</application>
Copy the code

Androidmanifest.xml in oldapp directory:

<application>
    <activity android:name="com.lqr.oldapp.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</application>
Copy the code

C. configure sourceSets

After the previous two steps, oldApp’s MainActivity and its respective main entry registration manifest files are removed. Next, use sourceSets to specify Java and manifest files depending on the channel name:

sourceSets {
    sourceSets.all { sourceSet ->
        // project.logger.log(LogLevel.ERROR, "sourceSet.name = " + sourceSet.name)
        if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
            if (sourceSet.name.contains("china")) {
                sourceSet.java.srcDirs = ['support/entry/java']
                sourceSet.manifest.srcFile 'support/manifest/oldapp/AndroidManifest.xml'
            } else {
                sourceSet.manifest.srcFile 'support/manifest/newapp/AndroidManifest.xml'}}}}Copy the code

At this point, only one main entry will remain in the androidManifest.xml of the final APK package, perfectly satisfying the client’s requirements.

3, disambiguation

Q: Why did you remove the MainActivity from OldApp?

A: Because The MainActivity of OldApp is not only needed by the Channel China, but will be used by other channels in the future. In order to consider future reuse, the MainActivity is removed.

Q: Why does sourceSets check whether sourceset. name contains Debug or Release?

A: If you print sourceset. name, you will see that the output is not only the variants, but also androidTest, test, main, etc. But we only want to specify Java directories and manifest files for the project variants (chinaDebug, chinaRelease, freeDebug, freeRelease). If we specify “things” like test, main, etc., the result is not what we want, so, Make sure source is configured with the variant we want to specify, not the other one.

Q: What is the relationship between sourceSets and variant merging?

A: For example, the default Java source directory of the AS project is [SRC /main/ Java]. In Gradle, the sourceSets specify another directory, such AS [support/entry/ Java]. AS considers both directories to be valid Java directories, so the Java directory specified by sourceSets is merely an extension, not a replacement. Using the Java source directory as an example, if your project is configured with multiple channels, there are two valid Java directories, SRC /main/ Java and SRC/channel name/Java, when the project is packaged without considering the sourceSets. The merge rules for variants do not change depending on the sourceSets configuration, and if you take both of the above into account, there are three valid Java directories for final packaging, They are SRC /main/ Java, SRC/Channel name/Java, and Support/Entry/Java.

V. Channel dependence

Gradle provides (compileOnly), compile (API), implementation (dependencies), and provides (compileOnly) dependencies. For [Build Type], [Product Flavor], [Build Variant], these configurations also appear some combinations, such as:

A. Build type combinations

debugCompile	// All debug variants depend on
releaseCompile	// All release variants depend on
Copy the code

B. Multi-channel combination

chinaCompile	// Channel dependence in China
americaCompile	// America channel dependency
freeCompile		// Free channel dependency
Copy the code

C. Variant combination

chinaDebugCompile		// chinaDebug variant dependency
chinaReleaseCompile		// chinaRelease variant dependency
americaDebugCompile		// americaDebug variant dependency
americaReleaseCompile	// americaRelease variant dependency
freeDebugCompile		// freeDebug variant dependencies
freeReleaseCompile		// freeRelease variant dependency
Copy the code

1. Configure channel dependencies in a conventional way

Using the above combination, you can easily configure dependencies in various situations, such as:

// autofittextview
compile 'me. Grantland: autofittextview: 0.2 +'
// leakcanary
debugCompile "Com. Squareup. Leakcanary: leakcanary - android: 1.6.1." "
debugCompile "Com. Squareup. Leakcanary: leakcanary - support - fragments: 1.6.1." "
releaseCompile "Com. Squareup. Leakcanary: leakcanary - android - no - op: 1.6.1." "
// gson
chinaCompile 'com. Google. Code. Gson: gson: 2.6.2'
americaCompile 'com. Google. Code. Gson: gson: 2.6.2'
freeCompile 'com. Google. Code. Gson: gson: 2.5.2'
Copy the code

2. Configure channel dependencies in code

Although the official list of multiple combined dependencies can solve almost any dependency problem, in reality, when there are many, many channels, the entire Gradle file can become cumbersome. Can you imagine a situation where only 1 out of 20 channels depends on a different version of Gson? Take advantage of gradle as a script and use code for channel dependencies:

dependencies {
    gradle.startParameter.getTaskNames().each { task ->
        // project.logger.log(LogLevel.ERROR, "lqr print task : " + task)
        if (task.contains('free')) {
            compile 'com. Google. Code. Gson: gson: 2.5.2'
        } else {
            compile 'com. Google. Code. Gson: gson: 2.6.2'}}}Copy the code

In addition, there is another approach that I have used in previous projects that does not support relying on remote warehouse components, which is also noted here:

dependencies {
	// Configure plug-in library dependencies
    applicationVariants.all { variant ->
        if (variant.flavorName == 'china')
                ||variant.flavorName == 'america') {
            dependencies.add("${variant.flavorName}Compile".project(':DroidPluginFix'))}else {
            dependencies.add("${variant.flavorName}Compile".project(':DroidPlugin'))}}}Copy the code

Remember, the following is correct, but it doesn’t work:

dependencies.add("${variant.flavorName}Compile".'com. Google. Code. Gson: gson: 2.6.2')
Copy the code

DroidPluginFix is the latest official DroidPlugin adapted to Android7 and 8, while DroidPlugin is not adapted. Due to historical reasons, it is necessary to rely on different versions of DroidPlugin for different channels.

Six, the concluding

Spent one and a half years in the company, this time is very strict with their own, learn a lot of new knowledge, and boldly into practice, harvested quite a lot, because of the particularity of the company, grasp of the gradle and multi-channel requirement is higher, so, this is my part of this time to focus on learning, but, after all, is to learn the knowledge alone, There may also be some cases that are not in place, so if the knowledge mentioned above is wrong or there is a better way to deal with it, welcome to point out and share