Compilation time is getting longer, time = life, I want to save.

The project framework

At the beginning, there was only one APP in the project. The project structure was very simple, that is, a business Module and a common base library.






Figure 1

With the development of the business, the second, third and even the NTH app, the project structure becomes as follows.





Figure 2

Different apps have common functions, so add a business base library and move the common parts into it.

This framework lasted for a long time, but as the business grew rapidly, the number of developers increased, and the code became more and more bloated, some problems began to emerge. Just because multiple apps are not conducive to promotion, the project begins to develop towards the direction of cloud platform, which requires an integrated platform APP and adds the original APP as a business module into the platform APP.

What’s the problem

The biggest problem with the old framework was the expansion of the business base library, and the common functions needed by any two apps could only be moved into the business base library. After a long time, the library became unreadable. Everything in it was directly referenced by all apps. The coupling was severe.

  • The business base library will only get bigger and bigger, with functions simply grouped into folders that no one can fully grasp.
  • Code just keeps getting bigger and bigger, and it takes longer and longer to compile;
  • To modify a function, we have to test every app that calls this function, and the test cost is high.
  • Multiple developers modify the business base library, resulting in more code conflicts and high communication costs;
  • Direct reference code, lack of interface and encapsulation, inflexible business iteration.

In short, the whole project must be demolished.

Componentization practice

Today’s search is two kinds of componentization and plug-in, both of which are discussed and analyzed a lot. The biggest benefit of plug-in is the ability to dynamically modify the code. If this dynamic capability is not required, it is not recommended to consider plug-ins. Official does not recommend things, there is no need to wade in muddy waters, support plug-in libraries, are domestic manufacturers.

The biggest goal of this component-based transformation is to reduce the dependence between codes and make each business module relatively independent, so that the developers of the original business APP can focus more on their own part, without the need for full engineering compilation.

Take it apart and end up like this:






Figure 3

  • The base library is business independent and can be applied to any project;
  • The business base library has a base Module that refers to the base library. Then, under the base Module, several functional modules are divided according to their functions. The next layer selectively includes according to its own needs;
  • The business APP layer is the main improvement of componentization, which will be analyzed later.
  • There is only one MainApp Module in the platform APP, which is a shell project without any business code and is the carrier of the final business APP integration.
Application and Library

For apps that can run independently, the module property is Application and is defined in build.gradle as:

apply plugin: "com.android.application"
Copy the code

Build. gradle does not run independently and provides dependencies on other modules called library.

apply plugin: 'com.android.library'
Copy the code

The basic library and the business base library are both library and MainApp are application. For business apps, there is a distinction between the development phase and the integration phase. In the development stage, it is hoped that the business app can run independently; In the integration stage, it is hoped that the business APP will change and be integrated into the platform APP in the form of library.

The switch between the development phase and the integration phase can be made available to Module by defining global variables in Gradle. I define a config.gradle, which is introduced at the root build.gradle:

apply from: "config.gradle"
Copy the code

Config. gradle is used to manage configurations, version numbers, and dependent libraries in a centralized way.

Ext {buildBizApp = false // Whether to build separate service app compileSdkVersion = 24 buildToolsVersion = "26.0.1" minSdkVersion = 15 TargetSdkVersion = 19 versionCode = 1 versionName = "1.0.0" dependencies = [supportV4: 'com. Android. Support: support - v4:24.2.1', appcompatV7: 'com. Android. Support: appcompat - v7:24.2.1', recyclerviewV7: 'com. Android. Support: recyclerview - v7:24.2.1', the design: 'com. Android. Support: design: 24.2.1']}Copy the code

In build.gradle of business app, the variable buildBizApp is judged to achieve the purpose of free switching.

if (rootProject.ext.buildBizApp) {
    apply plugin: "com.android.application"
} else {
    apply plugin: 'com.android.library'
}
Copy the code
2, photo of the module

Here’s an example of a module that takes pictures and views pictures. It provides CameraActivity and PhotoActivity. This is very basic functionality, and if it was a pre-componentized framework, I would throw it into the business base without hesitation, because all apps need it.

Direct references between modules are convenient to use, but not conducive to long-term code maintenance. After all, code boundaries can be easily broken by relying only on package name partitioning. The best approach is compile isolation, with no direct references between the caller and the Photo Module.

By switching between Application and Library, we can achieve the following effect.





Figure 4.

For the Photo Module, in addition to CameraActivity and PhotoActivity, DebugMainActivity is added. When the Photo Module is separately compiled into a Photo app, the DebugMainActivity is the home page. When mainApp needs to be compiled, the Photo Module is packaged into mainApp as the Library and other modules. Note that the DebugMainActivity is useless at this point, only the orange box part is required. Red arrow jumps can no longer be performed with explicit intents. Instead, jumps can be performed with implicit intents, or with routes described later.

The benefits are obvious: modules are completely decoupled. During the development phase, you can compile the Photo app separately and debug the photo and view images in the DebugMainActivity, saving the time of compiling the entire app and clicking tests in the app.

The implementation of the above pattern requires modification of the configuration, which will be explained step by step.

3. Merge AndroidManifest

Each module has androidmanifest.xml. Obviously, when a module is in application and library, it needs different Androidmanifest.xml.

  • Library: Defines CameraActivity, PhotoActivity, and DebugMainActivity, where the DebugMainActivity is defined as the start page.
  • Application: You only need to define CameraActivity, PhotoActivity, and eventually merged into the Androidmanifest.xml of mainApp, which describes what pages the Photo Module provides.
sourceSets { main { if (rootProject.ext.buildBizApp) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { Manifest. SrcFile 'SRC/main/AndroidManifest. XML'/resource/remove the debug Java {exclude 'debug / * *}}}}Copy the code

Androidmanifest.xml file location: androidmanifest.xml file location: androidmanifest.xml file location: build.gradle For unneeded test files like DebugMainActivity, you can put them in the Java/DEBUG folder and exclude them in integration mode.

4. Application processing

Similar to Androidmanifest.xml, there is only one Application at the end of the runtime, and the Application class also distinguishes between development mode and integration mode.

  • Development mode: The Photo Module has its own Application, called PhotoApplication, that initializes third-party libraries or adds other operations. Corresponding PhotoApplication need to define to the debug/AndroidManifest. XML, PhotoApplication file into the Java/debug.
  • Integration mode: PhotoApplication is only useful when the Photo Module is compiled separately. After integration, you need to define the MainApplication in the MainApp as the ultimate unique Application.

It is easy to imagine that you would also need a BaseApplication, a parent of PhotoApplication and MainApplication, to provide public initialization methods and global Context retrieval.

5. Route redirection

Because modules are split, explicit Intent jumps between pages are not possible. Implicit intents can be used, but writing is cumbersome, and some section-oriented features are hard to implement, so I don’t use them.

I used the library alibaba/ARouter that supports the routing function. The project has detailed documentation and demo, I won’t copy and paste, here is how I use it.

First define the address for the CameraActivity and add comments directly to the class:

@Route(path = "/photo/activity/camera")
Copy the code

Then write it like this where it needs to be called:

Bundle bundle = new Bundle();
//set bundle

ARouter.getInstance()
       .build("/photo/activity/camera")
       .with(bundle)
       .navigation(activity, BizConstant.RequestCode.TAKE_PHOTO);
Copy the code

With the path, parameters, and requestCode defined, it is easy to implement a route jump.

Is the jump sure to succeed? If you compile a business app separately during the development phase, the Photo Module does not exist, nor does the pre-login Module, as shown below:





Figure 5

If the photo module does not exist, Arouter provides a Callback function:

public abstract class NavCallback implements NavigationCallback {
    public NavCallback() {
    }

    public void onFound(Postcard postcard) {
    }

    public void onLost(Postcard postcard) {
    }

    public abstract void onArrival(Postcard var1);

    public void onInterrupt(Postcard postcard) {
    }
}
Copy the code
  1. If the jump fails, you can return some test data directly, and the Photo Module should be a stable component maintained by someone else without wasting clicks during development.
  2. If you need to test call the Photo Module, you can only enable integration mode, but you can manually change the configuration of MainApp, because modules in the business App layer can be arbitrarily combined, including only those used, minimizing compilation time.

As for the front login, it is a must. It is annoying to input the account password, and I do not want to integrate the Login Module in the development stage. What can I do?

Recall that the module of each business APP layer has its own Application in the development stage, which can be put into the simulated login process. This is an idea to write at a time and benefit from for a few months.

Dependency injection

Dependency injection should be familiar to you, and it’s a great way to decouple code. Learn on your own if you don’t.

Android has a famous dependency injection framework called Dagger, but I don’t use it, but Arouter also has dependency injection, which is good enough.





Figure 6.

In MVP mode, a view and presenter are identical, so you can create a new presenter object in your Activity.

The Model layer is divided into various services, such as TaskService, UserService, and SettingService, which only expose interfaces. This is where dependency injection is appropriate, where the Presenter only needs to hold a reference to the service and the instance is injected by Arouter.

Similar to the Activity definition path, the service implementation class defines annotations:

@Route(path = "/test/service/task")
public class TaskServiceImpl implements TaskService {}
Copy the code

Then in Presenter, annotate the Service that needs to be injected with Autowired. In the example, there are two methods, one is global injection, the other is a single injection, according to the actual situation.

@Autowired
TaskService taskService;
@Autowired
UserService userService;

public MyPresenter() {
    ARouter.getInstance().inject(this);
    //taskService = ARouter.getInstance().navigation(PollingTaskService.class);
    //userService = ARouter.getInstance().navigation(UserService.class);
}
Copy the code

The new process is omitted, but here’s a more complicated example.





Figure 7.

Select User Displays the list of users and provides the function of selecting users. However, only the corresponding caller knows the generation method of the list of users. Dependency injection can be used to simplify code in cases where caller and Select users are already componentized.

First, define an interface BaseSelectUserService with a method listUser() in their common business library layer. Caller1 and Caller2 implement the BaseSelectUserService interface respectively to complete their listUser() logic.

BaseSelectUserService selectUserService = 
(BaseSelectUserService) ARouter.getInstance().build(servicePath).navigation();
Copy the code

The key is to inject the appropriate SelectUserServiceImpl object for select User, where servicePath is the argument passed by the page jump, so that the caller’s listUser() can be called correctly. If there is a third caller, we need to implement the BaseSelectUserService interface and pass the service path.

7. Database

The project database uses SQLite and the ORM framework is greenDAO. Before componentization, each business APP maintains its own database, and the relationship between databases should be considered after integration.

Database tables are divided into two types, one is the common table, such as user table, resource table; The second is the business table of each business APP. After componentization, there are three directions:

  1. Business apps still maintain their own databases;
  2. Extract the common table to the upper business base layer, unified management;
  3. All tables are placed in the business base layer.

The second one is ideal in module division. The common table is in the business base layer, and the business table is maintained in the respective business APP. It is reasonable, but there are obstacles. GreenDAO defines an object as a table via @Entity. At compile time, the tables in different modules generate DaoMaster and DaoSession respectively. In other words, each module has a database. Multiple table queries across databases are tricky, so don’t use the second option.

The first is the least changed, but since the common table is not defined in the business base library, all the logic of the common table needs to be implemented in the business app, which I cannot accept.

The third one is left. Although all the business tables need to be defined in the business base library, it is not good, but it is only the table definition. The logic of adding, deleting, changing and checking is still in the business APP, so it is not good to use it first without modifying the database variety.

8. Resource conflict

Multiple modules are developed by multiple developers in parallel, which inevitably leads to duplicate resource names. There are bound to be conflicts when they are eventually merged into MainApp, and the best way is to define a prefix for the resource. For example, all resources in the Photo Module are prefixed with photo_.

Build. gradle can add a configuration that forces resources to specify a prefix.

android{
       resourcePrefix "photo_"
}
Copy the code

This configuration can only limit resources in XML files, so get in the habit of adding images.

conclusion

A lot of work has been done, but it is still in the “early stages” of componentization. Component service exposure, complete code isolation, component life cycle, component communication, component testing, and a whole host of other areas that could be improved, will be explored in the following sections.

Thanks to the efforts and selfless sharing of many online gods, I have learned a lot. If you have any questions or better methods, please contact us.