First, the problem of code centralization

When splitting a large project into modules or new componentized projects, the expectation is that the modules will have a horizontal relationship with each other. In this way, the business is relatively concentrated and everyone can focus on one thing. At the same time, the coupling degree of the code will be reduced to a high degree of decoupling, because there is no dependency between modules of the same level, and they are isolated in compilation. This will make the dependencies between components very clear, and at the same time, there is a higher reuse. Components emphasize reuse, while modules emphasize the division of responsibilities. They don’t have very strict divisions.

A module that is reusable is a component. Substitutability, hot swap, independent compilation of each component would be feasible,

1.1 Problems of code centralization in Android componentization

It looks like Android componentization is very simple and feasible. AS provides a way to create modules and custom properties of Gradle. properies are readable. Or ext’s globally configurable project property, or Kotlin’s syntactic sugar in the Kotlin DSL, provides a switch between application and library.

Then putting the code in different repository locations — preferably with administrative isolation at a separate Git repository level — will achieve the set of problems we want to address.

However, things are not as simple as imagined…

A number of problems followed, the most significant for me was the use of mapped databases in application design, which led to reuse problems in integration and component patterns. The end result is code generation using annotations in conjunction with Java features, which is not perfect but still solves this problem. Just as I cheer for the victory of the moment, a “wechat Android modular architecture reconstruction practice” article into my eyes.

An important and urgent problem arises, the problem of code centralization

How did this problem arise? In the wechat Android modular architecture refactoring practice is described in this way

“” “

However, as the code continues to swell, some problems begin to emerge. The first problem is the basic project libnetscene and libplugin. The foundation project has been in a state of constant expansion, while the main project is also growing. At the same time, there is a centralization problem in the basic engineering, many business Storage classes are attached to a core class, over time this class has become unreadable. In addition, in order to smooth the switch to Gradle and avoid too many changes to the structure and too many modules, we connected all the projects to one module. Without compile isolation, code boundaries between modules have deteriorated somewhat. While tools were subsequently developed to limit false dependencies between modules, the impact of this time has been felt. As a result of these problems, many modules are no longer “independent.” So when we look at the code architecture again, what used to be a good modular architecture design has gradually changed.

“” “

Let’s look at their analysis of the reasons for the problem:

“” “

Looking at the code of basic engineering, we can see that in addition to the supporting components such as storage and network, there are quite a lot of business-related code. This code is a source of bloat. But how did the code get here? Everything that is unreasonable has a logic behind it. In the previous architecture, we used the Event Event bus a lot as the way of communication between modules, and it was basically the only way. Using an Event as a communication medium naturally requires a place to define it so that each module knows what the Event structure looks like. The foundation project seems to be the only place to store events — the Event definition is placed in the foundation project; Then, what if A module A wants to use module B’s data structure class? Put the class down to basic engineering; Module A wants to return data from an interface of module B. Then sink the code down to the basics…

In this way more and more code is “naturally” sinking into the infrastructure.

If we look at the main project, it expands for a different reason. The analysis basically confirms that, first of all, as the backbone business, there has been demand development, expansion is inevitable, the lack of appropriate internal refactoring is not the core of the problem for the time being. Part of the reason is that the lifecycle design of the module seems to have outlived its usefulness. The previous module lifecycle was from “Account initialized” to “Account logged off,” so you can see that there must be logic beyond this timing. This was not a big problem in the past, since there is not a lot of logic to execute before “Account initialization” starts. Now, however, simple logic can add up to complexity. At this point, logic outside the module lifecycle is essentially confined to the master project.

In addition, module boundary destruction, the centralization of basic engineering, are the accomplice of the continuous deterioration of the code…

“” “

After reading it, I fell into a deep thought. Isn’t this the problem we face? This problem occurs not only in componentization, but in many scenarios where dependency relationships are formed.

Suppose you have a User build and share component. The share component requires the User component to provide data.

How does it work? Let’s look at a set of pictures:

1.1.1 figure 1

The solution is that the shared component depends on the User component, which can solve the problem. Suppose that there is A component A, which needs to reference the shared component, must depend on the shared component and the User component. This breaks the vision of component compilation isolation, and componentization will lose its flavor.

1.1.2 figure 2

The common data in the User component is transferred to the Base component, and the sharing component depends on the Base component to achieve data provision. However, when a large number of components need to provide data to each other, the centralization problem will occur. The B component that only needs to share components has to rely on the Base component and introduce other data. Thus, code centralization and subsidence lose the significance of componentization.

How to solve the problem of code centralization

Wechat in the face of this grievous issue issued “you have disease in cou justification, not to treat will fear deep” feeling, but also issued a very severe operation -.API

This operation is very advanced and the practice is very Tencent. However, this document only mentions the essence without specific operation steps, so there are still challenges for us.

2.1 What is the API solution to the code centralization problem

So let’s see what the process looks like,

In Figure 3, we use some technology to abstract the part of user component that needs to share data into an interface, use AS configuration of file type to change (kotlin) to.api, and then create a modole-API component with the same package name for other components to rely on.

The shared component relies on it in module mode with other components and with its own components, which makes it perfectly possible to use the data that needs to be shared separately.

2.1.1 SPI implementation

This is similar to the Service Provider Interface (SPI) mechanism. For details, see:www.jianshu.com/p/46b42f7f5…

(From the above document)

This means that we can first abstract the shared data into the interface to form a standard service interface, and then implement that interface in the concrete implementation, and then implement that interface in the corresponding block. When the service provider provides a concrete implementation of the interface, In the meta-INF /services directory of the jar package, create a file named “interface fully qualified name”, which contains the fully qualified name of the implementation class.

We then use the ServiceLoader to load the implementation specified in the configuration file, at which point we load the required files between the different components using the ServiceLoader

2.1.2 use ARouter

Using ARouter to pass data between components + Gralde automatically generate modulo-API components, forming a central problem. API

Assuming that we satisfy all of these relationships and build them correctly, how do we handle communication between components,

Arouter Ali communication route

@Route(path = "/test/activity")
public class YourActivity extend Activity {... } jump: arouter.getInstance ().build()"/test/activity").withLong("key1".666L).navigation()
Copy the code
// Declare the interface through which other components invoke the service

public interface HelloService extends IProvider {

   String sayHello(String name);

}

// Implement the interface

@Route(path = "/yourservicegroupname/hello", name = "Testing Service")

public class HelloServiceImpl implements HelloService {

    @Override

    public String sayHello(String name) {

        return "hello, " + name;

    }

    @Override

    public void init(Context context) {

    }

}

/ / test

public class Test {

    @Autowired

    HelloService helloService;

    @Autowired(name = "/yourservicegroupname/hello")

    HelloService helloService2;

    HelloService helloService3;

    HelloService helloService4;

    public Test() {
    
        ARouter.getInstance().inject(this);

    }

    public void testService() {

    // 1. (Recommended) Use dependency injection to discover services. Annotate fields and use services without actively acquiring services

    // The Autowired annotation will inject the corresponding field in byName mode. If the name attribute is not set, the service will be discovered in byType mode by default (if there are multiple implementations of the same interface, the service must be discovered in byName mode).

    helloService.sayHello("Vergil");

    helloService2.sayHello("Vergil");

    // 2. Use the dependency search method to discover services and take the initiative to discover and use services. The following two methods are byName and byType

    helloService3 = ARouter.getInstance().navigation(HelloService.class);

    helloService4 = (HelloService)ARouter.getInstance().build("/yourservicegroupname/hello").navigation();

    helloService3.sayHello("Vergil");

    helloService4.sayHello("Vergil"); }}Copy the code

What if the user information of the user component needs to be used by the payment component?

ARouter can communicate through the IProvider injection service above, or through EventBus

data class UserInfo(val uid: Int.val name: String)
/ * * *@author kpa

*@date 2021/7/21 2:15 下午

*@email billkp@yeah.net

*@descriptionUser login, access to information, etc. */

interface IAccountService : IProvider {
    // Obtain account information *
    fun getUserEntity(a): UserInfo?
}

// Inject the service

@Route(path = "/user/user-service")
class UserServiceImpl : IAccountService {

    / /...

}
Copy the code

In the payment component

IAccountService accountService = ARouter.getInstance().navigation(IAccountService.class);

UserInfo bean = accountService. getUserEntity();
Copy the code

Where does the IAccountService and UserInfo in the payment component come from?

This is the problem that module-API needs to solve, in terms of principle:

  1. Design the class files that need to share data and initialize data as.api files

Open AS-> Prefernces -> File Types and find kotlin (Java). Select “.api” from File Name Patterns.

For example:

UserInfo.api

data class UserInfo(val userName: String, val uid: Int)
Copy the code

UserService.api

interface UserService {

fun getUserInfo(a): UserInfo

}
Copy the code
  1. Generate a module-API component that contains the shared data and class files that initialize the data

This can be done in the following ways:

  • Creating a module-API component by hand is obviously not desirable but feasible
  • Use the script language shell or Python to scan the specified path and generate the corresponding module-API
  • Using The Android compilation environment and groovy language to write Gradle scripts, the advantage is that you do not need to consider when to compile, do not break the compilation environment, writing is simple

3. Module-api script

After finding out the principle of these problems and how to implement them, I found the scripts provided by excellent people on Github, which fully met our expectations

def includeWithApi(String moduleName) {

def packageName = "com/xxx/xxx"

    // Load the module normally first

    include(moduleName)

    // Find the path of the module
    String originDir = project(moduleName).projectDir
    // This is the new path
    String targetDir = "${originDir}-api"
    // The name of the original module
    String originName = project(moduleName).name
    // Name of the new module
    def sdkName = "${originName}-api"
    // This is the location of the public module. I put a new API. Gradle file in it
    String apiGradle = project(":apilibrary").projectDir
    // Delete the files before each compilation
    deleteDir(targetDir)

    // Copy the.api file to the new path
    copy() {
        from originDir
        into targetDir
        exclude '**/build/'
        exclude '**/res/'
        include '**/*.api'
    }
    // Copy the public module's AndroidManifest file directly to the new path as the module's file
    copy() {
        from "${apiGradle}/src/main/AndroidManifest.xml"
        into "${targetDir}/src/main/"
    }
    // Copy the gradle file to the new path as the module's Gradle
    copy() {
        from "${apiGradle}/api.gradle"
        into "${targetDir}/"
    }

    // Delete the empty folder
    deleteEmptyDir(*new* File(targetDir))
    // Replace todo with its own package name
    // Create a new path for AndroidManifest. The path is to create an API package under the original package and use it as the package name in AndroidManifest
    String packagePath = "${targetDir}/src/main/java/" + packageName + "${originName}/api"
    // Replace todo with its own package name. In this case, the Apilibrary module copy of AndroidManifest, replace the package name inside
    // Modify the AndroidManifest package path
    fileReader("${targetDir}/src/main/AndroidManifest.xml"."commonlibrary"."${originName}.api")

    new File(packagePath).mkdirs()
    // Rename gradle
    def build = new* File(targetDir + "/api.gradle")

    if(build.exists()) {
        build.renameTo(new File(targetDir + "/build.gradle"))}// Rename the.api file to generate a normal.java file
    renameApiFiles(targetDir, '.api'.'.java')
    // The new module is loaded normally
    include ":$sdkName"
  }

private void deleteEmptyDir(File dir) {

    if(dir.isDirectory()) {
        File[] fs = dir.listFiles()
        if(fs ! =null && fs.length > 0) {
            for (int i = 0; i < fs.length; i++) {
                File tmpFile = fs[i]
                if (tmpFile.isDirectory() {
                    deleteEmptyDir(tmpFile)
                }
                if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0){
                    tmpFile.delete()
                }
          }
       }
   if (dir.isDirectory() && dir.listFiles().length == 0) {
        dir.delete()
   }
 }

private void deleteDir(String targetDir) {

    FileTree targetFiles = fileTree(targetDir)

    targetFiles.exclude "*.iml"

    targetFiles.each { File file ->

        file.delete()

    }

}

/**

* rename api files(java, kotlin...)

**/

private def renameApiFiles(root_dir, String suffix, String replace) {

    FileTree* files = fileTree(root_dir).include("**/*$suffix")

    files.each {

        File file ->

        file.renameTo(*new* File(file.absolutePath.replace(suffix, replace)))

    }

}

// Replace field * in AndroidManifest

def fileReader(path, name, sdkName) {

    def readerString = ""

    def hasReplace = false

    file(path).withReader('UTF-8') { reader ->

        reader.eachLine {

            if (it.find(name)) {
                it = it.replace(name, sdkName)
                hasReplace = true
            }
            readerString <<= it
            readerString << '\n'

        }
        if (hasReplace) {

            file(path).withWriter('UTF-8') {
                within ->
                within.append(readerString)
            }

        }

    return readerString

    }

}
Copy the code

Use:

includeWithApi ":user"
Copy the code

Democomponent-api

References:

“Wechat Android Refactoring Modular Architecture Refactoring Practice”

Gradle implementation API Solution

Advanced Development Needs to Understand the SPI Mechanism in Java