Summarize some knowledge points of Android modularization.

Summarize some knowledge points of Android modularization.

I have something to say about Android modularization.

Recently, a project of our company used modular design. I participated in the development of a small module, but the overall design was not designed by me. It took more than half a year to develop, and I would like to record my thoughts here.

Modular scenes

Why do we need modularity?

When the number of App users increases and the business volume grows, many development engineers will be involved in the same project. With the increase in the number of people, the original small team development method is no longer suitable.

The original code now needs to be maintained by multiple people. The quality of each person's code is different, which makes code review more difficult and prone to code conflicts.

At the same time, with the increase in business, the code becomes more and more complex, and the code coupling between each module becomes more and more serious. The decoupling problem needs to be solved urgently, and the compilation time will also become longer and longer.

As the number of staff increased, each business had to implement its own set of components, resulting in different UI styles and technical implementations for the same App, and the team's technology could not be consolidated.

Architecture evolution

At the beginning, the project architecture used the MVP model, which is also a very popular architecture in recent years. The following is the original design of the project.

As the business grows, we add the concept of Domain. Domain gets data from Data. Data may be Net, File, Cache, and other IOs. Then the project architecture becomes like this.

Then, as the number of employees increased, various basic components became more and more numerous, the business became more complex, and there was a strong coupling between businesses, and it became like this.

After using modular technology, the architecture becomes like this.

Technical points

Here is a brief introduction to the technologies and technical difficulties needed to achieve modularization in Android projects.

Library module

Before starting modularization, you need to extract each business into an Android Library Module. This is a built-in function of Android Studio, which can extract basic components with fewer dependencies into a separate module.

As shown in the figure, I separated each module into a separate project.

Use gradle to add code dependencies in the main project.

  1. // common
  2. compile project( ':ModuleBase' )
  3. compile project( ':ModuleComponent' )
  4. compile project( ':ModuleService' )
  5.  
  6. // biz
  7. compile project( ':ModuleUser' )
  8. compile project( ':ModuleOrder' )
  9. compile project( ':ModuleShopping' )

Library module development issues

When extracting code into separate Library Modules, you will encounter various problems. The most common one is the R file problem. In Android development, each resource file is placed in the res directory. During the compilation process, an R.java file will be generated. The R file contains the ID corresponding to each resource file. This ID is a static constant, but in the Library Module, this ID is not a static constant, so you must avoid such problems during development.

For example, the same method handles click events of multiple views. Sometimes, switch(view.getId()) is used, and then case R.id.btnLogin is used for judgment. At this time, problems will arise because id is not a constant, so this method cannot be used.

Also, during development, the most commonly used third-party library is ButterKnife. ButterKnife cannot be used either. When using ButterKnife, you need to use annotations to configure an id to find the corresponding view, or bind the corresponding event handlers. However, the assignment of each field in the annotation also requires static constants, so ButterKnife cannot be used.

There are several solutions:

1. Re-create a Gradle plug-in and generate an R2.java file. Each id in this file is a static constant, so it can be used normally.

2. Use the most primitive method provided by the Android system, directly use findViewById and setOnClickListener methods.

3. Set up the project to support Databinding, and then use the objects in Binding, but the number of methods will increase significantly. At the same time, Databinding will also have compilation issues and learning costs, but these are also minor problems, and I personally do not think they are a big problem.

The above are the mainstream solutions. My personal recommended priority is 3 > 2 > 1.

After the modules are separated, everyone can group the corresponding modules separately, but there will be resource conflicts. My suggestion is to add prefixes to the resource names of each module. For example, if the login interface layout in the user module is activity_login.xml, it can be written as us_activity_login.xml. This can avoid resource conflicts. At the same time, Gradle also provides a field resourcePrefix to ensure that the names of each resource are correct. For specific usage, please refer to the official documentation.

Dependency Management

After completing the Library module, the code is basically clear and similar to the final architecture above. It has the most basic skeleton, but it is still not complete because multiple people still operate the same git repository, and each development partner still needs to perform various forks and pr on the same repository.

With the segmentation of the code, the main project app has more dependencies. If the code in the lib is modified, the compilation time will be very scary. I roughly counted that when it was in the same module, the compilation time was about 2-3 minutes, but after separation, it took about 5-6 minutes, which is absolutely unbearable.

The above problem can be solved by using a separate git repository for each sub-module. In this way, everyone only needs to focus on the git repository they need. The main repository uses git submodule to depend on each sub-module separately.

However, this still cannot solve the problem of long compilation time. We also package each module separately. After each sub-module is developed, it is published to the Maven repository, and then the version is used for dependency in the main project.

For example, if we are iterating a certain version, this version is called 1.0.0, then the versions of each module are also called the same version. When the version is tested and released, each module is tagged with the corresponding version, and then the code distribution of each module can be clearly understood.

The gradle dependencies are as follows.

  1. // common
  2. compile 'cn.mycommons:base:1.0.0'  
  3. compile 'cn.mycommons:component:1.0.0'  
  4. compile 'cn.mycommons:service:1.0.0'  
  5.  
  6. // biz
  7. compile 'cn.mycommons:user:1.0.0'  
  8. compile 'cn.mycommons:order:1.0.0'  
  9. compile 'cn.mycommons:shopping:1.0.0'  

Some people may ask, since each module has been developed separately, how to carry out joint development debugging? Don't worry, this question will be kept for the time being and will be discussed later.

Data Communications

When a large project is split into several small projects, the calling method changes slightly. I have summarized several ways of data communication between the various modules of the App.

  • Page jump, for example, when placing an order on the order page, it is necessary to determine whether the user is logged in. If not, it is necessary to jump to the login interface.
  • Actively obtain data. For example, when placing an order, the user is already logged in, and the order requires the user's basic information to be passed.
  • Passively obtain data. For example, when switching users, sometimes you need to update the data. For example, on the order page, you need to clear the shopping cart data of the original user.

Let’s take a look at the App’s architecture.

The first problem is that the original method is to directly specify the ActivityClass of a certain page, and then jump through intent. However, in the new architecture, since the shopping module does not directly depend on the user, the original method cannot be used for jumping. Our solution is to use Router routing jump.

The second problem is that the original method has a dedicated business unit, such as UserManager, which can be called directly. However, due to the change of dependencies, it cannot be called. The solution is to define all the required operations as interfaces and put them in the Service.

The third question is that the original method can provide a callback interface for event changes. When I need to monitor an event, I can just set the callback.

Page route jump

As analyzed above, the original method code is as follows.

  1. Intent intent = new Intent(this, UserActivity.class);
  2.  
  3. startActivity(intent);

But after using Router, the calling method has changed.

  1. RouterHelper.dispatch(getContext(), "app://user" );

What is the specific principle? It is very simple. Just do a simple mapping match and pair "app://user" with UserActivity.class. Specifically, define a Map, the key is the corresponding Router character, and the value is the Activity class. When jumping, get the corresponding ActivityClass from the map, and then use the original method.

Some people may ask, how to pass parameters to another page? No problem, we can add parameters directly after the router. If it is a complex object, we can serialize the object into a json string, and then get the corresponding object from the corresponding page by deserializing it.

For example:

  1. RouterHelper.dispatch(getContext(), "app://user?id=123&obj={" name ":" admin "}" );

Note: The json string in the router above needs to be url encoded, otherwise there will be problems. This is just an example.

In addition to using Router for redirection, I thought about it and decided to refer to the Retrofit method and directly define the redirection Java interface. If additional parameters need to be passed, they can be defined as function parameters.

This Java interface has no implementation class. You can use the dynamic proxy method, and the following method is the same as using Router.

So what are the advantages and disadvantages of these two methods?

Router method:

  • Advantages: No need for difficult technical points, easy to use, directly use string definition jump, good backward compatibility
  • Disadvantages: Because it uses string configuration, it is difficult to find bugs if the characters are input character by character, and it is also difficult to know the meaning of a certain parameter.

Retrofit-like method:

  • Because it is a Java interface definition, it is easy to find the corresponding jump method. The parameter definition is also very clear and can be written directly in the interface definition for easy reference.
  • Also, because it is a Java interface definition, if you need to expand parameters, you can only redefine new methods, which will result in multiple method overloading. If you modify the original interface, the corresponding original caller must also make corresponding modifications, which is troublesome. The above are two implementation methods. If you have students who want to implement modularization, you can make a choice based on the actual situation.

Interface and Implement

As analyzed above, if we need to obtain data from a certain business, we need to define the interface and implementation class respectively, and then instantiate the object through reflection when obtaining it.

Here is a simple code example

Interface Definition

  1. public interface IUserService {
  2.  
  3. String getUserName();
  4. }

Implementation Class

  1. class UserServiceImpl implements IUserService {
  2.  
  3. @Override
  4. public String getUserName() {
  5. return   "UserServiceImpl.getUserName" ;
  6. }
  7. }

Reflection Generator Object

  1. public class InjectHelper {
  2.  
  3. @NonNull
  4. public   static AppContext getAppContext() {
  5. return AppContext.getAppContext();
  6. }
  7.  
  8. @NonNull
  9. public   static IModuleConfig getIModuleConfig() {
  10. return getAppContext().getModuleConfig();
  11. }
  12.  
  13. @Nullable
  14. public   static <T> T getInstance(Class<T> tClass) {
  15. IModuleConfig config = getIModuleConfig();
  16. Class<? extends T> implementClass = config.getServiceImplementClass(tClass);
  17. if (implementClass != null ) {
  18. try {
  19. return implementClass.newInstance();
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. return   null ;
  25. }
  26. }

Actual call

  1. IUserService userService = InjectHelper.getInstance(IUserService.class);
  2. if (userService != null ) {
  3. Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show();
  4. }

In this example, each call uses reflection to generate a new object. In actual applications, it may be used in combination with IoC tools, such as Dagger2.

EventBus

For the third question above, the original design is also acceptable. You only need to define the callback interface into the corresponding service interface, and then the caller can use it.

But I suggest you use another way - EventBus. EventBus also uses the observer mode to monitor events, which is a more elegant way to set callbacks.

Advantages: There is no need to define many callback interfaces, only the event Class needs to be defined, and then the event matching can be performed through the claas's ***.

Disadvantages: Many additional classes need to be defined to represent events, and attention should also be paid to the life cycle of EventBus. When events are not needed, event bindings need to be unregistered, otherwise memory leaks may occur easily.

<<:  ARKit & OpenGL ES - ARKit principle and implementation

>>:  Microservice architecture: PaaS cloud platform architecture design based on microservices and Docker container technology (microservice architecture implementation principles)

Recommend

They captured 0.54 seconds of romance!

🌕🌕🌕🌕🌕🌕 Images of China's space station "...

Are you a master programmer or a novice programmer?

[[131541]] "Rookie" and "Master&qu...

Do you get goosebumps when you see "goosebumps"? It's not redundant at all...

Not only can he stand up to warm you in the cold,...

Nokia N9 is not dead: it can boot up three operating systems

As the last champion of Nokia's MeeGo platform...

6 steps to quickly get started with growth hacking

Growth hacking is not a new concept in China. How...

"Dry Goods" Complete Guide to Internet Finance New Media Marketing Plan!

New media is a relative concept. There will alway...

Why can your activities attract fans but fail to retain them?

The purpose of organizing activities is not to co...

The reward war escalates into a lose-lose game between Apple and APP developers

Following WeChat, Apple has finally taken action ...

Bud parenting class: 10 abilities to let children take control of their lives

This is a course that teaches parents how to put ...

Fitness APP operation: turn users into little fairies who stick to you

In recent years, the fitness trend has been on th...