The main module/submodule channel correspondence relationship in gradle is realized through configuration

Foreword:

During our development process, we often face scenarios where different codes and resources need to be generated for different channels. At present, Google actually provides us with a set of channel package solutions, which are briefly described here.

For example, my main module depends on module1 and module2. If two channels A and B are declared in the main module, then we can also choose to create corresponding channels A and B in module1 and module2. In this way, when the main module selects A, the corresponding sub-module will automatically switch to channel A. At this time, the channels of the main module and the channels of the sub-module are in one-to-one correspondence, as shown in the following figure:

This configuration provided by Google can meet most scenarios. But if the number of modules I depend on is particularly large, a new problem will arise. The channels of the main module and submodules do not correspond one to one. For example, as shown in the figure below, both channel A and channel B depend on channel A of module 2, but channel A depends on channel A of module 1, and channel B depends on channel B of module 1. What should I do at this time? The core of this article is to introduce how to solve the corresponding relationship between main module/sub-module channels in this complex scenario.

1. Requirements sorting

The middle right of the picture above is actually just a simple example. The actual scenario encountered by the author is much more complicated than this example. It is undoubtedly unwise to write this complex relationship directly in build.gradle. We should write it in the form of a configuration file to dynamically generate this dependency. This will not only facilitate subsequent maintenance, but also look more intuitive.

So first of all, in terms of design, I divided the configuration file into two parts:

1. Channel package declaration of submodule. As declared in module-flavors in the xml below, there are two submodules. The channels of the submodule module-map are market1 and market2, and the channels of the submodule module-adapter are market1 and market2 (markettet1 and market2 here can be configured to be inconsistent).

2. Main module dependency part. As declared in project-flavors in the xml below. For example, in the channelB channel of the main module, the market1 channel of module-map and the market2 channel of module-adapter are used.

<?xml version="1.0" encoding="utf-8" ?><!-- 渠道依赖配置表 -->
<flavors-config>
    <module-flavors name="module-flavors">
        <module-flavor module-name="module-map">
            <flavor name="market1" />
            <flavor name="market2" />
        </module-flavor>
        <module-flavor module-name="module-adapter">
            <flavor name="market1" />
            <flavor name="market2" />
        </module-flavor>
    </module-flavors>
    <project-flavors name="project-flavors">
        <flavor name="channelA">
            <flavor-item name="module-map" flavor-name="market1" no-use="true" />
            <flavor-item name="module-adapter" flavor-name="market1" />
        </flavor>
        <flavor name="channelB">
            <flavor-item name="module-map" flavor-name="market1" />
            <flavor-item name="module-adapter" flavor-name="market2" />
        </flavor>
    </project-flavors>
</flavors-config>

Therefore, the entire requirement needs to implement the following functional points:

1. Declare the channels of the corresponding submodules in XML and the corresponding relationship between the main module/submodule;

2. The build.gradle of the submodule introduces the configuration and uses the submodule channel configured in XML to dynamically generate productFlavors;

3. Based on the xml configuration in the main module, the corresponding main module channel is generated, as well as the sub-module channels that the main module channel depends on.

4. Processing in some extreme scenarios. For example, main module channel A depends on two modules 1 and 2, while main module channel B depends on three channels 1, 2, and 3. This asymmetric relationship is compatible with the processing.

Next, we will divide it into several chapters to explain these functional points one by one.

2. Sub-module dynamically generates channels according to configuration

The xml has been listed in Chapter 1, so I will use it directly here. To realize the dynamic generation of channels for sub-modules, we split it into two steps:

First, the xml configuration must be dynamically read into the memory during the Sync process and the corresponding object must be generated;

Secondly, the corresponding gradle configuration is dynamically generated based on the corresponding object.

2.1 Read configuration in XML

To implement the first function point, we can first create a flavor_build.gradle file, and then declare a Map type object MODULE_FLAVOR in it to store the channel correspondence.

ext {
    //以下属性通过plugin_of_flavor.xml配置
    def moduleFlavor = new HashMap()
    MODULE_FLAVOR = moduleFlavor
}

Then use XmlParser to load the configuration file, parse the file to generate the corresponding object and add it to MODULE_FLAVOR.

def xmlParser = new XmlParser()
//读"渠道依赖配置表",并转换为Map
def xml = xmlParser.parse("${getRootDir().getAbsolutePath() + File.separator}plugins_of_flavor.xml")
xml.get("module-flavors").'module-flavor'.each { Node moduleNode ->
    def moduleName = moduleNode.attribute("module-name")
    def flavors = []
    moduleNode.value().each { Node pluginNode ->
        flavors.add(moduleName.replace("module-", "") + "-" + pluginNode.attribute("name"))
    }
    MODULE_FLAVOR.put(moduleName, flavors)
}

The final effect should be similar to the following code:

moduleFlavor.put("module-adapter", ["adapter-market1", "adapter-market2"])

In the representative module module-adapter, there are two channels: adapter-market1 and adapter-market2.

2.2 Dynamically generate channels in sub-modules

Switch to the submodule's build.gradle, first introduce flavor_build.gradle, and then automatically generate the corresponding channel through the following code.

apply from: '../flavor_build.gradle'

android {
    ...
    productFlavors {
        MODULE_FLAVOR.get(project.name).each {
            "${it as String}" {
                println("-------------> flavor: " + it)
            }
        }
    }
}

At this point, the first requirement has been fulfilled.

3. Main module channel generation and corresponding relationship configuration with sub-modules

Still divided into two steps:

1. Read configuration from XML

2. Generate the corresponding configuration items in build.grdale of the main module

3.1 Reading configuration from XML

This process is actually similar to that in 2.1, but there are some differences in the data structure.

ext {
    def projectFlavor = new HashMap()
    PROJECT_FLAVOR = projectFlavor
}
def xmlParser = new XmlParser()
def xml = xmlParser.parse("${getRootDir().getAbsolutePath() + File.separator}plugins_of_flavor.xml")

xml.get("project-flavors")."flavor".each { Node flavorNode ->
    def flavorName = flavorNode.attribute("name")
    def flavors = []
    flavorNode.value().each { Node flavorItemNode ->
        def items = []
        items.add(flavorItemNode.attribute("name"))
        items.add(flavorItemNode.attribute("flavor-name"))
        flavors.add(items)
    }
    PROJECT_FLAVOR.put(flavorName, flavors)
}

The final effect is actually the same as the following code:

PROJECT_FLAVOR.put("bux", [["module-map", "market1"], ["module-adapter", "market1"]])

3.2 Generate corresponding configuration items in the main module

The first is the dependency statement of the main module, which represents dependence on the two modules module-adapter and module-map.

dependencies {
    implementation project(':demo-common')
    implementation project(':module-adapter')
    implementation project(':module-map')
}

Then generate the channel in the closure of android

android{
    flavorDimensions "channel"
    PROJECT_FLAVOR.each { flavorName, configList ->
        productFlavors.create(flavorName) {
            dimension "channel"
            matchingFallbacks = configList.collect { subList ->
                return subList.take(2).collect { it.replace("module-", "") }.join("-")
            }
        }
    }
}

In fact, the above code is to dynamically generate a configuration similar to the following after sync is completed:

android{
    flavorDimensions "channel"
    productFlavors {
        channelA {
            dimension "channel"
            matchingFallbacks = ['map_market1', 'adapter_market1']
        }
        channelB {
            dimension "channel"
            matchingFallbacks = ['map_market1', 'adapter_market2']
        }
    }
}

In this way, channelA of the main module will be designated to use the map_market1 channel of map and the adapter_market1 channel of adapter. The same goes for channelB.

Speaking of people, some people will also ask why not use configuration for configuration. for example

implementation project(path: ':module_map', configuration: 'market1')

I have also tried this. GPT and Baidu have such plan instructions, but when I actually ran it, I found that the channel code in the corresponding module module_map was not entered at all. After trying it for a day, I found that this plan did not work.

4. Processing of asymmetric scenes

If main module channel A depends on two modules 1 and 2, and main module channel B depends on three channels 1, 2, and 3, how to deal with this asymmetric relationship?

In my opinion, although channel A does not depend on module 3, it does not affect the logic if module 3 is also included. I just need to get rid of the routing code in the corresponding routing class.

So the simplest solution is that I can dynamically configure and generate different BuildConfig during compilation, so that I can perform corresponding processing according to different configurations in BuildConfig.

For example, if I add the no-use option in xml, it means not to use it.

<project-flavors name="project-flavors">
    <flavor name="channelA">
        <flavor-item name="module-map" flavor-name="market1" no-use="true" />
        <flavor-item name="module-adapter" flavor-name="market1" />
    </flavor>
    <flavor name="channelB">
        <flavor-item name="module-map" flavor-name="market1" />
        <flavor-item name="module-adapter" flavor-name="market2" />
    </flavor>
</project-flavors>

Then read this configuration in flavor.gradle:

xml.get("project-flavors")."flavor".each { Node flavorNode ->
    def flavorName = flavorNode.attribute("name")
    def flavors = []
    flavorNode.value().each { Node flavorItemNode ->
        def items = []
        items.add(flavorItemNode.attribute("name"))
        items.add(flavorItemNode.attribute("flavor-name"))
        items.add(flavorItemNode.attribute("no-use"))
        flavors.add(items)
    }
    PROJECT_FLAVOR.put(flavorName, flavors)
}

Subsequently, the corresponding BuildConfig is generated in the build.gradle of the main module.

productFlavors.all { flavor ->
    def moduleList = PROJECT_FLAVOR[flavor.name]
    def sb = new StringBuilder("{")
    moduleList.each {
        //no-use为true时不生成对应的模块配置
        if (it[2] == 'true') {
            return
        }
        sb.append("\"").append(flavor.name).append("\"").append(",")
    }
    sb.append("}")
    buildConfigField("String[]", "PLUGIN_IMPL_ClASSES", sb.toString())
}

In this way, if it is channelA channel, its BuildConfig content is as follows:

public static final String[] PLUGIN_IMPL_ClASSES = {"module-adapter",};

channelB channels are as follows:

public static final String[] PLUGIN_IMPL_ClASSES = {"module-adapter","module-map",};

The specific how to use it is the function in the routing class, so I won’t go into details here.

5. Reference materials

https://juejin.cn/post/6976508673027735588

Guess you like

Origin blog.csdn.net/AA5279AA/article/details/133497338