Since last year, more and more apps have started componentized refactoring. There are a lot of good componentization blogs out there, so this post is not about componentization, but how we can break down an old project and refactor it step by step.

The idea of componentization is good, but not all projects are suitable for using a componentized way of development, so it is generally necessary to use a componentized project. They are generally characterized by a long period of project iteration, large and bloated projects, large communication costs among project team members, and complex project maintenance costs. This is where componentization comes in.

Others are small projects with few people and simple functions. Don’t think about componentization directly. You just have to be honest with yourself. Forcing componentization only increases maintenance and development costs. The gains are not worth the losses

Componentized structure

Componentization has never been a refactoring thing, before componentization. It is best to have a basic understanding of the structure of componentization:

Above is the most basic structure of componentization. You can sort of see that. Componentization is mainly divided into three layers:

  1. App shell:

    This is a componentized running container. The shell defines the app entry and relies on the business components for running.

  2. Business components:

    This is the middle layer of componentization, in a large project group. There are different business groups, such as login, shopping, video and so on. These different business groups maintain a separate business component to achieve the effect of business decoupling of their respective business groups.

    In principle: there should be no direct dependencies between business components! All business components need to be able to operate independently. For testing, when you need to rely on the capabilities of multiple business components for integration testing. You can use the App shell for multi-component dependency management.

  3. Base components:

    Base components are also called base functional components. These components provide basic functional support for upper-layer service components. Such as the basic network component, the basic view component, the basic data access component, and the componentized core communication component: the routing component.

The above is the most basic structure of componentization, of course, in a real project, there is no such a simple structure, it needs to be expanded according to your specific situation. For example, you can add a layer of special functional components between the base component and the business component. Functional components in this layer can only be dependent on one or more components, as long as the bottom-up one-way dependency chain is not broken:

To prepare

Component-based refactoring is never about refactoring. You need a strong leader to support the implementation before you can actually refactor.

Second, you need to make a hierarchical division of the overall direction of your project ahead of time, which things need to be placed in which layers. There needs to be a clear division in advance.

Refactoring your base components: Your basic functional framework needs to be split out of the project ahead of time. Including network, image loading, data storage, buried point, routing and so on.

Establish a componentized project structure

Build the base assembly of components

You need to create a basic Library Module. Used to rely on all the underlying functional components, such as Baselib.

Function: It is used to unify the dependent basic function library, coordinate and associate the relationship of each function framework, and do the initialization and encapsulation operation of each function library. Provides direct invocation by upper-level business components.

Create business components and app shells for each line of business

This is different from the common componentization approach, which uses a variable for control. Enables business components to switch flexibly between application and library. Make the component also an Application. Application is also a component.

But this is done because you are always switching between libraryModule and applicationModule. It can easily lead to a variety of confusing issues: Manifest conflict, R file conflict, etc.

Therefore, we adopted the method of multi-app shell group loading:

You can see the business components for each line of business. Each has its own app shell module. The main app shell depends on all the business components. When doing business development. Members of each service group can directly run their respective APP shell modules for testing, and the main APP shell is fully packaged.

At the beginning of the split, it is suggested to use the original project Application as the main app shell.

Set aside a core business component. For example, login components: These components are business components, but are required by all other components, so they are set apart as core business components. Then the other business components. Through their respective app shell projects. Dependencies can be entered: Again remind that business components cannot be directly dependent:

The benefits of this hierarchical structure are:

  1. Business components no longer switch between library and application. Unified development environment, not easy to appear environment switching conflict
  2. The app shell stands on its own. It is possible to add unique independent code to the shell project, since the shell function will not participate in the main app shell to compile, so you can do this for their own business. Add some separate entry management classes. For example, add a RootActivity to add a list that can jump to any page to facilitate test run, etc.

Gradle unified configuration management

After componentized refactoring, there are more modules, so it is necessary to perform unified configuration management of gradle scripts for all modules. Avoid confusion.

  • Create a script named dependencies. Gradle. Add unified dependency version number management:
ext {
    COMPILE_SDK_VERSION = 25
    BUILD_TOOLS_VERSION = '25.0.0'
    MIN_SDK_VERSION = 16
    TARGET_SDK_VERSION = 19
    
    // SUPPORT
    SUPPORT_VERSION = '23.2.0'
    SUPPORTDEPS = [
            supportV4 : "com.android.support:support-v4:${SUPPORT_VERSION}".supportV13 : "com.android.support:support-v13:${SUPPORT_VERSION}".appcompatV7 : "com.android.support:appcompat-v7:${SUPPORT_VERSION}".cardview : "com.android.support:cardview-v7:${SUPPORT_VERSION}".design : "com.android.support:design:${SUPPORT_VERSION}".annotations : "com.android.support:support-annotations:${SUPPORT_VERSION}".multidex : 'com. Android. Support: multidex: 1.0.1']... }Copy the code

This script manages all version data in a unified manner. When the external user needs to use the version number and dependency, the user needs to uniformly read from the configuration properties of this file. For example, to rely on the supportV4 package:

compile "${SUPPORTDEPS.supportV4}"
Copy the code
  • Define baseConfig. Gradle. Configure basic compilation scripts for components in a unified manner
boolean isAppModule = project.plugins.hasPlugin('com.android.application')
android {
    compileSdkVersion Integer.parseInt("${COMPILE_SDK_VERSION}")
    buildToolsVersion "${BUILD_TOOLS_VERSION}"

    lintOptions {
        abortOnError false
    }
    defaultConfig {
        if (isAppModule) {
            applicationId "com.haoge.component.demo"
        }
        minSdkVersion Integer.parseInt("${MIN_SDK_VERSION}")
        targetSdkVersion Integer.parseInt("${TARGET_SDK_VERSION}")

        versionCode Integer.parseInt("${DEFAULE_CONFIG.versionCode}")
        versionName "${DEFAULE_CONFIG.versionName}"}}Copy the code

Like this. You can use the Apply syntax. Let all components module. All rely on this Gradle script. The unified environment is configured.

If you’re careful, you can find it. In baseConfig, I added the default applicationId designation. That’s because for most applications. Have used various third-party SDKS. In particular, third party login, this KIND of SDK framework. In many cases, package name validation is required, so it is recommended to add the default applicationId here.

If there is too much trouble and hands-on ability. Consider wrapping your own Gradle plugin for unified configuration management

Adds a resource prefix to the component

We need to set their own resource prefixes for each component as naming constraints to avoid problems such as compilation conflicts caused by different components naming different resources with the same name.

android {
    resourcePrefix 'lg_'
}
Copy the code

This resource prefix is used to alert you when you create a resource name under this module if the name does not match the prefix. Avoid conflict.

Unified management of large file resources and picture resources

After componentization. Resource management is also a problem, image resources, assets resources, raw file resources, etc. Both have the characteristics of occupying large resources and rarely modifying the basic. So it’s better to break it down here. Unified for all components to use:

Therefore, consider placing such large file resources in the lowest level of componentization. So that different components do not have to maintain a separate copy of this large file resource. Avoid wasting resources. For example, this part of the resources can be directly. Directly into baselib, as a base function to provide libraries for use.

Distribute applications for each component

One might ask: Why do application lifecycle dispatch for components?

Here’s an example: Everybody knows. Network libraries and image loading libraries need to be initialized before they can be used. However, in componentization, they should not distribute their respective applications. The initialization of a unified process cannot be performed. Then maybe your component A needs to manually write the initialization of the base library itself. Component B and Component C are also required. Finally your main app shell also needs this time. It’s easy to mess up!

So you have to have a structure. To make the respective components. Complete the function initialization of each component.

For example, the basic function components: initializing the network, picture framework, etc., and the upper-layer business component A, initializing its own other function operations. Each component initializes only its own part of the operation. Regardless of what other dependent components need to be initialized.

For this part of the lifecycle distribution, refer to the baselib delegate class in demo:

The demo link is at the end of the article.

Intercomponent communication

Routing communication

The core of inter-component communication is the routing framework, which needs to be placed in the basic functional components at the lowest level to provide the upper layer for use. Here I use my own routing framework, Router: a single, componentization, plug-in fully supported routing framework.

This routing framework can be used in single, componentized and plug-in. This Router is recommended if you want to be able to easily switch from a componentized to a plug-in environment if you need it later

If you already have your own routing framework in your project that directly supports componentized environments. It’s best not to consider changing this one. To be honest, switching to a routing framework is a lot of work.

Since almost all of the blogs that introduce componentization have very detailed descriptions of the routing framework, I will not elaborate on this section. If you are interested, please refer to the link above for more information.

Event communication

Different from routing communication, routing is mainly used for interface hop communication and has little effect on ordinary event communication. Let’s say I’m component A and I need to call an interface in component B and get the returned data to operate on. At this point, you need another way to implement it.

A lot of people talk about event communication. You might remember to use EventBus. Yes, EventBus is a great framework for event communication, but anyone who has used it knows that. Once EventBus is abused. Over time, because of its unique decoupling characteristics, your code is difficult to debug and maintain.

So at this point, we abandoned the use of EventBus as a bridge for time communication between components. It’s simply using inversion of control. The communication protocol between components is defined in the underlying component, and the upper-layer business components respectively implement the corresponding protocol interfaces of the underlying layer to communicate.

Let’s take the login component as an example:

First, add a protocol interface to the underlying component layer. This interface is used to define the time communication entry provided by the login component, such as logout, cookie clearing, and so on:

public interface LoginPipe extends Pipe{
    void logout(a);
    void clearCookie(a);
}
Copy the code

Then. In the login component. Implement this protocol interface. And register with the corresponding communication manager:

// Implement the protocol interface.
public final class LoginPipeImpl implements LoginPipe {
    @Override
    void logout(a) {
        // do something
    }
    
    void clearCookie(a) {
        // do something}}Copy the code
// Register this implementation in the protocol manager
// PipeManager is also located in the base component.
PipeManager.register(LoginPipe.class, new LoginPipeImpl());
Copy the code

It can then be used in other components. Through the PipeManager protocol manager. According to the protocol class. Get the corresponding implementation class and call it directly:

PipeManager.get(LoginPipe.class).logout();
Copy the code

The above approach, though simple indeed, has the following advantages:

  • Improve the cohesion of each component protocol. It is more suitable for each component to manage and maintain their own protocol interfaces in a unified manner.
  • The implementation scheme is simple and easy to understand, easy to debug.
  • In the componentized split process, it is convenient to delete the main APP shell independent code in the later stage.

The last one may be a bit more complicated. So let’s expand on this:

We mentioned it above. During componentized refactoring of older projects. Use the main module as the main app shell, and the app shell actually needs no specific business code. So there’s a conflict here. But our componentized split can not be done overnight, only slowly step by step, page by page to split and test. So it’s a long and painful process.

In the process of unbundling, it is hard to avoid the awkwardness of both old and new code. And this awkward scene will continue until all the components are broken down.

Then there’s the other problem with the split: the split plans of the business groups are not synchronized, which means it’s very likely that the business you’re currently splitting. The function needs to be called to another business group, and this function at this time. It probably hasn’t been submitted to the split schedule at all. So at this point. You have to call the logic from the old project directly in your split component first.

So use the above event communication mechanism. You will need to suggest a temporary protocol interface in the main app. Such as:

public interface MainPipe {
    void doSomething(a);
}
Copy the code

The main project then implements and registers it. For your component to use. This is also true when other components encounter similar problems. In this MainPipe type continue to add the corresponding communication protocol method and implement.

Because of this practice. Will all the main app temporary protocol interface. Are placed in this MainPipe. Improved protocol cohesion. When all business groups have completed componentized refactoring. Then we can refactor the MainPipe directly and migrate the protocols of each component to the protocol class of each component. Then we can safely delete the unrelated business code in the main app. Make it a real main app shell project.

Data communication

Most of the time, components actually communicate with each other. The data passed is plain, simple data, but sometimes. You need to pass complex data. For example, when making cross-component API calls and getting the data back, or when reading the user’s complete data.

Taking reading the complete data of users as an example, the protocol definition of data communication is still the event communication mechanism of the above parties as the implementation carrier:

public class User { String uid; String nickname; String email; String phone; . }Copy the code

The User class contains all the User information. Then now you need to pass this user instance across components. You need to define a protocol method. Provides access to this User instance:

public interface LoginPipe {
    User getUser(a);
}
Copy the code

This is normal practice, but to do so, you need to copy the User instance into the protocol customization layer, the underlying component.

In the development process, this phenomenon is very common. And many times, as the requirements change, the data that needs to be passed is different. It is also not possible to migrate the corresponding entity bean into the protocol customization layer every time. It would be too much trouble.

So this approach to cross-component communication. The recommended approach is to use JSON data for data communication

Json communication mechanism, can perfectly avoid entity bean migration problems. It also allows the receiver to parse and read the data on demand:

For example, the component that I receive. Currently, only the nickname and UID data are required. I don’t care about the other data. Then I can just parse the data for these two fields. Do on-demand parsing.

Speaking of which. I recommend another of my frameworks, Parceler, which encapsulates Bundle access operations. Automatic conversion of JSON is also supported. Specific usage can refer to my another blog, interested can see.

Parceler: This is the elegant way to use bundles for data access! (At the end of the article, there are instructions on how to use the framework under componentization and plug-in.)

To optimize the speed

As the componentized split refactoring proceeds. You will find that the components under the project are broken down more and more, even though you have broken down the granularity of the components. It’s been controlled. But the fact that modules continue to increase after componentization is indisputable, this time. As the number of modules continues to increase. Your project’s compile time will also skyrocket.

We know that. The first step in the project compilation process is to package and compile all library modules. Generate the corresponding AAR. The APP waits for all modules to be packaged and then decompresses the AAR. Resources, code merge, and package into apK execution run.

So we made a Gradle accelerator. This is used to pre-compile the Module with aar. Skip the module packaging aar process. Achieve the effect of compilation speed.

For details, see this article: Speedup: a plugin designed to Speedup Library projects

More tips

Because in a componentized development environment, you’re going to have a lot more problems than just these, and of course these are the main ones.

So here’s the tip section for adding some of the things we usually do when we’re developing. Problems you might encounter. In other words, some coding advice for certain circumstances. (These points probably won’t be reflected in the demo, so please read the description carefully.)

Use opportunely ActivityLifecycleCallbacks do initialization

Because componentization has a characteristic: each business group can choose its own development mode, such as MVP, MVVM,RN, etc.

Android componentized Demo