Author: Guo Xiaoxing

Proofread by: Guo Xiaoxing

Article status: completed

About the project

The BeesAndroid project aims to provide a series of tools and methods to lower the threshold of reading the Android source code, so that more Android engineers can understand the Android system and master the Android system.

The article directories

  • As soon as the problem is identified
  • Ii. Proposal
    • 2.1 Module Containers
    • 2.2 Module Architecture
    • 2.3 Module Communication
    • 2.4 Module life cycle
    • 2.5 Module initialization
  • 3. Solving problems

Modularity is also a technical point often mentioned in recent years. The reason is that with the gradual expansion of the company’s business, the engineering volume of the main application has gradually become larger, and management and compilation have become very difficult. In addition, with the development of the company’s business, it is inevitable to split the main application function and the r&d team, which requires that each module in the main application can be compiled and run independently, without coupling with the main project and other modules.

The process of modularization is actually a process to solve the technical debt, each company’s technical debt is also different, because the process of modularization is a process of local conditions, there is no universally applicable plan, generally speaking, modularization is divided into the following three steps:

  1. Find the problem: find the problem is to clarify the company’s current technology structure, clean up the technical debt.
  2. Proposal: The proposal includes two aspects, one is to test the new engineering architecture, the other is to do a good job of scheduling new requirements (whether it will block the new requirements).
  3. Problem solving: The implementation of the new scheme is also carried out gradually, the new module should do a good job in gray release, application rollback and other work.

However, the practice of modularity is not a simple thing. Every application has its own special situation, and there is no universally applicable technical solution. On the whole, the splitting of modules involves engineering framework (MVP), module communication (in-process, cross-process), Library multiplex reuse, resource splitting and other situations. So what is the ultimate goal of modularity? 🤔

  • Other modules of the main application can be quickly migrated to other applications.
  • Reduce Build time, each module to be independently responsible for each team, code responsibility system.
  • Each module of the main application can be divided into independent applications, and module functions are servized.
  • Modules can be developed, compiled, and run independently without any main engineering environment, and can be quickly replaced between modules.
  • Non-intrusive configuration of various independent services, such as: account information, Settings information, network services, image loading services, burial services, pull-down refresh style, error status, etc.
  • The Library can be used quickly and easily in multiple applications. Library functions as independent as possible in View or Fragment, when using can be directly added to the host Activity, host Activity can add its own download refresh style, Action bar style.

After understanding the specific modularity requirements, we began to really start modularity, only talk without practice, empty talk does not make any sense. The following modules are around our main application of big windmill and spread out.

The commercial breaks are at 😎

Large wind turbines: http://dafengche.souche.com/

It is a SaaS product, providing solutions such as station building system, ERP, CRM, wechat marketing system and financial system, aiming to help car dealers and 4S group to improve their operation and management level.

Before analyzing the program, first of all, we need to know what’s wrong with our application. In view of the big windmill project, we will make a specific analysis.

As soon as the problem is identified

The big windmill was launched in 2015. After three years of development, its business has grown greatly and its functions have been gradually improved. The milestones of the big windmill are as follows:

Like other teams, our main engineering structure is constantly changing with the development of our business. Here is a brief summary:

  1. Micro project: In the early days, it was a project with a few people. At that time, it was also a time of business volume. There was no special attention to the problem of architecture.
  2. Small projects: With the development of the business, the business types are gradually increasing. At this time, we split some business modules into independent Library, which is divided into a Base Library, providing some tools and styles.
  3. Medium project: The business is growing further, the simple Module Library has become difficult to use, this time plug-in framework is very popular, very powerful, but also a lot of problems, we finally adopted the Router way to achieve a pseudo modular solution.
  4. Large projects: time to now, the company business with explosive growth, the company also have the original of the application of two into five, and still has a lot of custom App, shadow App, module App needs to submit to us, such as in a pseudo modular solution, on the basis of we want to achieve a truly modular solution.

The structure of the great windmill is shown in the figure below:

You can see that the main project of the whole big windmill can be divided into four levels:

  • Main engineering business layer
  • Module business layer
  • Corporate framework layer
  • Third party framework layer

So you can see that the dependencies between projects and modules, and between modules, are really picturesque 😅. Mutual references lead to poor scalability and maintainability, and are difficult to test. Let’s look at what’s wrong with this kind of project architecture:

  • Module boundaries are broken, modules are interdependent, module upgrades are complex, and testing is difficult.
  • The basic engineering is centralized, and the class library is too heavy to maintain.
  • Modules depend on the main project, and all modules cannot be compiled and published independently. Compilation takes time, and APK is huge, so multiple teams cannot develop in parallel.

Ii. Proposal

Let’s take a look at the refactored architecture, as follows:

The refactoring of the windmill uses a multi-container architecture. Let’s see how this architecture is implemented.

2.1 Module Containers

Since we want to modularize the business, we need to have containers to carry modules. Currently, we mainly use the following three containers:

  • Native container: A container that is Native to Android/iOS. For example, Android has Activity containers, Fragment containers, and more granular View containers.
  • H5 container: Traditional WebView hosted pages.
  • ReactNative/Weex/Flutter container: This scheme has become popular since Facebook launched RN scheme in 2015. The idea of this scheme is to escape JS components into Native components, so as to achieve a set of interfaces and multi-terminal operation effect.

👉 Note: Android provides a fine-grained View container solution: Virtualview-Android, which dynamically renders views by delivering AN XML configuration file.

In the long run, these three sets of containers will not be used to replace each other, but will coexist for a long time to complement each other.

  • Native containers: Native containers are good for writing the basic skeleton pages of your app, such as the home page, and are also used on iOS to avoid audit issues.
  • H5 container: THE H5 container is suitable for writing pages that need to change frequently, such as business event pages.
  • ReactNative/Weex/Flutter containers: This type of container is ideal for writing regular page interfaces. Because this type of container also has a natural hot update capability, it can also be used for dynamic publishing, hot fixing, etc.

So how do you implement these three sets of containers? 🤔

  • Native container: plug-in solutions, plug-in solutions are generally similar, see my discussion of VirtualAPK in this article.
  • H5 container: WebView encapsulation, Jockey communication protocol encapsulation.
  • ReactNative/Weex/Flutter container: Construction of the engineering system of ReactNative/Weex/Flutter container. In fact, it is very simple to write pages using RN or Weex. The complexity lies in the construction of the engineering system.

We will discuss the implementation of each of these three sets of containers in detail in subsequent articles, and then we will look at the implementation of the module architecture.

2.2 Module Architecture

A good system design is vertically layered and horizontally modular. Let’s look at how to design a module from a vertical and horizontal perspective.

2.2.1 Vertical Architecture

Generally speaking, from the vertical perspective, a module can be divided into three parts:

  • Api layer: the interface part that provides external interfaces and data structures.
  • Implementation layer: The Implementation layer provides the Implementation of the business logic, which is often related to the state of the application, account information, etc. The library provides the specific functions for it, and it decides how to load, organize, and present these functions.
  • Library layer: the function layer, which provides some specific functions for implementation.

A module can thus be divided into three layers. For more complex modules, we also need to decouple and communicate between layers. Let’s look at how horizontal architecture can be implemented.

2.2.2 Horizontal Architecture

Horizontal architecture is how to deal with the relationship between view, data and business logic. As for the practice of this piece of content, from the original MVC, to MVP and MVVM, the purpose of various architectures is to hope that the module coupling is lower, the independence is stronger, and the portability is better.

Google itself has opened a Repo to discuss best practices for these frameworks, as follows:

  • android-architecture

  • MVC: There is an architecture scheme in the PC era, and it is also the earliest one on Android. The god role of Activity/Fragment not only assumes the role of V, but also the role of C. Small projects are very easy to develop, while large projects will encounter problems such as coupling too much and Activity/Fragment class too large.
  • MVP: To solve the MVC coupling problem, the core idea of MVP is to provide a Presenter to decouple view logic I from business logic.
  • The MVVM: Using a ViewModel instead of a Presenter is a two-way binding of data to a View. The earliest use of this framework is data-binding, which binds data to XML. This doesn’t work on a large scale, but data binding is a useful concept. Google then introduced the ViewModel component and the LiveData component. The ViewModel component specifies the status of the ViewModel, its lifecycle, production mode, and the problem of sharing ViewModel data with multiple fragments under an Activity. The LiveData component provides the implementation of View subscriptions to ViewModel data sources at the Java level.

Google also provides an official implementation of the MVP framework. The core idea of the MVP framework is as follows:

  • Use the Contract interface to manage the definitions of the View and Presenter interfaces. This does not have to be the case. Not every View and Presenter interface can be used in pairs. It is possible to have one VIew interface for each Presenter interface or one Presenter interface for each VIew interface.
  • Using Fragment to implement View interface, we know that Presenter interface mainly defines business logic, such as: Loading the next page, pull-down refresh, edit, submit, delete, etc., are all called in the page lifecycle method or setXXXListener. The Fragment lifecycle is suitable for use, and the Fragment can be independently filled into other activities.

There are two problems with the official framework:

  • As mentioned above, the View interface is implemented by fragments, but if a page is composed of multiple independent sub-pages, it is not reasonable to add several fragments to the page. In this case, we can take the next best thing, and use the way of custom View to implement the View interface.
  • When the page grows to a certain magnitude, there will be a large number of Presenter implementation classes. In fact, the existing project of Big Wind turbine has a lot of Presenter implementation classes, Presenter implementation class and View implementation class need to set each other, so that the View can call the Presenter to load data. A Presenter calls a View to refresh the UI, managing these Presenter classes is a big problem, and if someone inherits your View, you have to tell it how to handle the creation and destruction of a Presenter, when to load data and so on over the lifetime of the View. If there is cross-department or even cross-city cooperation, the communication cost is very high.

In general, when the volume of business increases, it is necessary to write a large number of View interfaces and Presenter classes, and it is necessary to synchronize the Presenter class with the Activity lifecycle, which can become very complicated for large projects.

In conclusion, an ideal solution is to combine the ViewModel component with the LiveData component to implement the MVVM framework.

  • todo-mvvm-live
  • Lifecycle Component official documentation

The framework has two important principles:

  • Any code that does not handle UI logic and user interaction should not be written to activities or fragments, because activities or fragments are fragile and can be destroyed due to low memory, configuration changes, going into the background, and so on. Reliance on activities or fragments should be minimized.
  • We should use a persistent data model to drive our UI. Data can be persisted in this model, and user data will not be lost if an Activity or Fragment is destroyed. This model is designed to handle data logic, to separate the application’s data logic from the view logic, and to make the application easier to maintain.

👉 note: Some of you may be wondering if there should be a Lifecycle component, if we can emulate the Lifecycle of an Activity or Fragment using the onAttachToWindow() and onDetachToWindow() methods of the View. In fact, View Lifecycle is unreliable in some special scenarios, such as RecyclerView and ViewPager, so we still need to use the Lifecycle component to listen for Activity or Fragment Lifecycle changes.

2.3 Module Communication

The problem of decoupling between modules is solved, and the other is the problem of communication between modules. In a large application, many modules can be run independently or even become an App independently, which involves data interaction and communication between modules. For example, the most common scenario is that submodules need to know the login information of the main application, etc. The communication between modules can be divided into two situations:

  • In-process communication: Modules all run in the same process.
  • Cross-process communication: Modules run in different processes.

2.3.1 Intra-process communication

There are various means of in-process communication, the most common being EventBus,

  • EventBus

EventBus is used to complete data interaction and communication between Activities, Fragments, Threads, and Services.

EventBus was a common way of communicating between pages and modules in the early days. The benefits of EventBus were obvious: it decoupled event publishers from subscribers without having to define a bunch of complex callback interfaces. But as the project grew, the problems became apparent.

  • Event is not the best way for all communication. It is mainly suitable for one-to-many broadcast scenarios. If the communication in the business requires a set of interfaces, multiple Events need to be defined and the code is complex.
  • A large number of Event classes are difficult to manage. If the application gets bigger and bigger and more modules are divided, this Event will become difficult to maintain.

But even so, EventBus is an excellent means of in-process communication.

👉 Note: In addition to EventBus, we can also choose LocalBroadcastReceiver for simple communication scenarios. A LocalBroadcastReceiver is a local broadcast within an application. It also uses a Looper Handler to maintain a global Map for communication within the application. Unlike EventBus, it sends strings. When LocalBroadcastReceiver is faced with service expansion, message string management is also a problem.

2.3.2 Interprocess communication

Cross-process communication can be done using Content providers,

  • Official document of the Content Provider

Content providers use the Binder mechanism to implement data interaction and communication between processes.

A common scenario is that multiple modules share the login information. The login information can be saved by the Content Provider. When the login status changes, each module can be notified.

Through the above analysis, we have completed a well-designed module, but the module access is still faced with many problems, such as: how to define the module life cycle, how to synchronize user information, how to register and initialize the module and so on. With a small number of modules, this is not a problem, but as the modules grow to a certain order of magnitude, the problem becomes very significant.

2.4 Module life cycle

The lifecycle of the module lifecycle can be divided as follows:

  1. Process start: Performs the initialization of the module.
  2. Account initialization: Synchronizes the module user information to notify the module that the user has logged in.
  3. Account logout: Synchronizes module user information and notifies module users that they have logged out.
  4. Process exit: The execution module exits.

2.5 Module initialization

Module initialization is generally carried out in the Application, of course, there are also lazy loaded modules. The initialization of modules generally passes Application context information, user information, configuration parameters and other information. Here, automatic initialization of modules can be considered, and the specific process is as follows:

  1. Add dependencies. There are also two types of dependencies: compile-time dependencies and run-time dependencies,
  2. Configure data, register services.
  3. Start the service.

3. Solving problems

Modular split is not a simple thing, can not be done overnight, it is not possible to let the team all stop to do the split and refactoring, so the real implementation of modularity needs to follow the following steps step by step.

  1. Attitude adjustment

Technical refactoring doesn’t bring short-term benefits, it’s a long-term benefit, you spend a lot of time doing it, it makes sense, but the boss doesn’t see it, and it doesn’t bring significant business growth. So the first thing is to do a good job of the ideological work of the team members.

In fact, most students in RESEARCH and development are still very technical pursuit, but our projects usually have a lot of historical problems, also known as the so-called technical debt, to reconstruct these things, the cost is very high, in the face of this situation, plus the usual business needs, time is tight, we usually think:

The difficulty of refactoring is so big that if something goes wrong, what should I do? Forget it, what others write, I also write how good.

This is a very common phenomenon, and in this case, an aggressive leader is needed to fire the first shot. After the first stage of refactoring, people will start to joke about how bad the original design is and how to design it.

  1. Module split: Split the modules that need to be refactored, including code, resources, etc.
  2. Grayscale publishing: Push refactoring to a small number of users.
  3. Application rollback: Tag your Git code and be ready to return if you run into problems.

The appendix

A few last words:

  1. Can use the native implementation do not use third-party library implementation, if you really need third-party library implementation, such as: photo library, network library, do not use directly, to do a good job of encapsulation and interface isolation, convenient replacement.
  2. Inheritance relationships between pages must be careful, and unless pages are designed specifically for inheritance, composition or other less intrusive approaches should be considered to solve the problem.
  3. When a project proposes a solution for a requirement that may be encountered by other teams, evaluate how coupled the solution is and whether it can be used directly by other teams in the future, with less duplication of effort between teams.
  4. Encapsulate the interfaces of the external functions as much as possible, and do not directly expose the internal details. In this way, the internal logic can be directly replaced in the future without affecting the business side.

This article ends here, welcome to pay attention to our BeesAndroid wechat public platform, BeesAndroid is committed to sharing Android system source code design and implementation of related articles, also welcome open source enthusiasts to participate in the BeesAndroid project.

Wechat public platform