04 project architecture – componentization – detailed explanation

Time:2022-5-24

1、 Background

With the gradual expansion of the project, there are more and more business functions, more and more code and more developers. During this process, have you ever had the following troubles?

  1. There are many and complex project modules. Compile for 5 minutes or even 10 minutes at a time? Too slow to bear?
  2. If you change a line of code or adjust only a little UI, you have to run the whole project and endure it for another 10 minutes?
  3. How often does the code conflict? Annoying?
  4. Someone secretly changed the code of their own module? Upset?
  5. Make a requirement and find that you have to change the code of many other modules?
  6. If you want to use similar functions that have been implemented in other modules, you can only copy a copy of the code and then change it?
  7. “I’m not responsible for this, I don’t care”. Is the scope of code responsibility unclear?
  8. Only the function of one module is done, but there are many modification points, so we need a complete regression test?
  9. Make a requirement, but unconsciously lead to bugs in other modules?

If you have these troubles, your project needs to be componentized.

In the first half of the year, my project underwent major reconstruction and completed component transformation. So I finally learned and practiced such a “high-end knowledge” and read some articles, so I have this article to summarize and share~

2、 Understanding of componentization

2.1 modularization

Before introducing componentization, let’s talk about modularity. We know that in Android studio, a new project has an app module by default, and then you can create a new module through file – > New – > new module. So the “module” here is basically the same concept as the “module”. In other words, originally an app module carried all the functions, butmodularizationIt is to split into multiple modules and put them in different modules. The code of each function is added in its own module.

Taking jd.com as an example, it can be roughly divided into six modules: “home”, “classification”, “discovery”, “shopping cart”, “my” and “commodity details”.

04 project architecture - componentization - detailed explanation

image.png

The project structure is as follows:

04 project architecture - componentization - detailed explanation

image.png

This is the structure that general projects will adopt. In addition, there is usually a common basic module module_ Common, which provides basic capabilities such as baseactivity / basefragment, image loading and network request, and then each business module will rely on this basic module. Is there any dependency between business modules? Obviously there is. For example, “home”, “classification”, “discovery”, “shopping cart” and “mine” all need to jump to “commodity details”, which must depend on “commodity details”; The “product details” need the ability to be added to the “shopping cart”; The “home page” click search is obviously the search function in the “classification”. So theseThere are complex dependencies between modules

04 project architecture - componentization - detailed explanation

image.png

Modularization is reasonable when each business function is relatively independent, but there will be many modulesPage JumpData transmissionMethod callAnd so on, so there must be the above dependency, that isThere is a high degree of coupling between modules。 With high coupling and large amount of code, the problems mentioned above are very easy to occur, which seriously affects the development efficiency and quality of the team.

In order to solve the problem of high coupling between modules, it is necessary toComponentizationYes.

2.2 introduction to componentization – advantages and architecture

ComponentizationRemove the coupling between modules, so that each business module can exist independently as an app without direct dependency on other modules.At this point, the business module becomesBusiness component

In addition to business components, there are also extracted business basic components, which are provided for business components, but not independent businesses, such as sharing components and advertising components; There are also basic components, that is, separate basic functions, which have nothing to do with business, such as image loading, network request, etc. These will be described in detail later.

Benefits of componentizationIt’s obvious:

  1. Speed up compilation: each business function is a separate project, which can be compiled and run independently. After splitting, the amount of code is less, and the compilation naturally becomes faster.
  2. Improve collaboration efficiency: decoupling makes components do not disturb each other, and the internal code of components is highly correlated. Everyone in the team has their own responsibility components, which will not affect other components; Reduce the cost for team members to be familiar with the project, and only need to be familiar with the responsible components; For testing, we only need to focus on the changed components, not the overall regression test.
  3. Function reuse: components are similar to the third-party library we refer to. You only need to maintain each component and build reference integration. Business components can be up and down, flexible and changeable; The basic components provide the basis for new business integration at any time and reduce the workload of repeated development and maintenance.

The picture below shows usExpected componentized architecture

04 project architecture - componentization - detailed explanation

image.png
  1. Component dependency is that the upper layer depends on the lower layer, and the modification frequency of the upper layer is higher than that of the lower layer.
  2. Basic componentsIt is a general basic capability with very low modification frequency. As an SDK, it can be used for the integration of all projects of the company.
  3. Common component, as the basis for supporting business components and business basic components (basic capabilities such as baseactivity / basefragment), it relies on all basic components to provide the basic functions required by most business components, and unifies the version number of basic components. Therefore, the basic capabilities required by business components and business basic components can be obtained only by relying on common components.
  4. Business componentBusiness basic components, all rely on common components. butThere is no dependency between business components, there is no dependency between business basic components. Business components depend on the required business basic components. For example, almost all business components rely on advertising components to display banner advertisements, pop-up advertisements, etc.
  5. The top layer is the main project, the so-called“Shell Engineering”It mainly integrates all business components, provides the unique implementation of application, gradle and manifest configuration, and integrates them into a complete app.

2.3 problem points of component development

We have understood the concept, advantages and architectural characteristics of componentization. To implement componentization, we must first find out what problems to solve?

The core issue isBusiness component decoupling。 So what is the coupling? As mentioned earlier, page Jump, method call and event notification. The basic components and business basic components do not have the problem of coupling, so they only need to be separated and encapsulated into a library. Therefore, there are the following problems for business components:

  1. How to run and debug business components separately?
  2. There is no dependency between business components. How to realize page Jump?
  3. There is no dependency between business components. How to realize communication / method call between components?
  4. There is no dependency between business components. How to obtain fragment instances?
  5. Business components cannot rely on shell engineering in reverse. How to obtain application instance and application oncreate() callback (for task initialization)?

Let’s see how to solve these problems.

3、 Component independent debugging

eachBusiness componentIt is a complete whole and can be regarded as an independent app. It needs to meet the requirements of separate operation and debugging, which can improve the compilation speed and efficiency.

How to debug components independently? There are two options:

  1. Single project scheme, the component exists in the form of module, and the engineering type of the component is dynamically configured;
  2. Multi engineering scheme, business components exist in the form of library moduleIndependent project, and there is only one library module.

3.1 single project scheme

3.1.1 dynamic configuration component engineering type

Single project mode: there is only one project in the whole project. It includes: app module plus each business component module, which is all the code. This is the single project mode. How to debug components separately?

We know that when Android studio develops Android projects, it uses gradle to build. Android gradle provides three plug-ins. Different module types can be configured by configuring different plug-ins during development.

  • Application plug-in, ID: com android. application
  • Library plug-in, ID: com android. library

The difference is relatively simple. The app plug-in configures an android app project, outputs an APK installation package after the project is built, and the library plug-in configures an Android library project, and outputs an arr package after the project is built.

04 project architecture - componentization - detailed explanation

image.png
04 project architecture - componentization - detailed explanation

image.png

Obviously, our app module is configured with the application plug-in, and the business component module is configured with the library plug-in. To implement business componentsIndependent debugging, you need to change the configuration to application plug-in; After independent development and debugging, you need to change back to the library plug-in for debuggingIntegrated debugging

How to make components automatically switch between these two debugging modes? Manually modify the grade file of the component and switch between application and library? If there are only two or three large components that can be modified manually, it may be cumbersome in more than a dozen business projects.

We know that after creating an Android project with Android studio, a gradle will be generated in the root directory Properties file. The constants defined in this file can be used by any build Gradle read. So we can go to gradle Define a constant value ismodule in properties, and true means independent debugging; False is integration debugging. Then in the business component build Read the ismodule from gradle and set it to the corresponding plug-in. The code is as follows:

//gradle.properties
#The components debug the switch independently and synchronize the project after changing the value each time
isModule = false
Copy code
//build.gradle
//Pay attention to gradle The data types in properties are all string types. Using other data types requires self conversion
if (isModule.toBoolean()){
    apply plugin: 'com.android.application'
}else {
    apply plugin: 'com.android.library'
}
Copy code

3.1.2 dynamic configuration of applicationid and androidmanifest

We know that an app needs an applicationid, and the component is also an app during independent debugging, so it also needs an applicationid. During integrated debugging, the component does not need an applicationid; In addition, an app has only one startup page, and components also need a startup page during independent debugging, which is not required during integrated debugging. Therefore, applicationid and androidmanifest also need ismodule for configuration.

//build.gradle (module_cart)
android {
...
    defaultConfig {
...
        if (isModule.toBoolean()) {
            //Applicationid is added during independent debugging and removed during integrated debugging
            applicationId "com.hfy.componentlearning.cart"
        }
...
    }

    sourceSets {
        main {
            //Separate debugging and integrated debugging use different androidmanifest XML file
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/moduleManifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
...
}
Copy code

It can be seen that ismodule is also used to set applicationid and androidmanifest respectively. The Android manifest debugged independently is a new directory, modulemanifest, which usesmanifest.srcFileYou can specify the androidmanifest file path for both debugging modes.

04 project architecture - componentization - detailed explanation

image.png

The newly created manifest file in modulemanifest specifies application and startup activity:

//moduleManifest/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_cart" >
    <application android:name=".CartApplication"
        android:allowBackup="true"
        android:label="Cart"
        android:theme="@style/Theme.AppCompat">
        <activity android:name=".CartActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
Copy code

The originally automatically generated manifest does not specify application or start activity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_cart">
    <application>
        <activity android:name=".CartActivity"></activity>
    </application>
</manifest>
Copy code

For independent debugging and integrated debugging, use “assemblydebug” to build respectively, and the results are as follows:

04 project architecture - componentization - detailed explanation

image.png

3.2 multi project scheme

3.2.1 scheme overview

Multi engineering scheme. Business components exist in the form of library moduleIndependent project。 Independent projects can be debugged independently, and the above configurations are no longer required.

For example, the shopping cart component is newly createdEngineering cartModule of_ Cart module, the business code is written in module_ Just in the cart. App modules are dependent modules_ cart。 The app module is just a component entry or some demo test code.

04 project architecture - componentization - detailed explanation

image.png

Then, when all business components are split into independent components, the original project will become a project with only app modulesShell Engineering, shell engineering is used to integrate all business components.

3.2.1 Maven reference component

So how to conduct integration debugging?Using Maven to reference components: 1. Release the ARR package of components to the Maven warehouse of the company. 2. Then use the implementation dependency in the shell project, which is the same as using the third-party library. In addition, the ARR package is divided into snapshot version and realease version. The snapshot version is used for debugging in the development stage, and the official version is used for official release. The details are as follows:

First, in module_ Create a new Maven in the cart module_ push. Gradle file, and build Gradle sibling directory

apply plugin: 'maven'

configurations {
    deployerJars
}

repositories {
    mavenCentral()
}

//Tasks uploaded to Maven warehouse
uploadArchives {
    repositories {
        mavenDeployer {
            pom. Version = '1.0.0' // version number
            pom. Artifactid = 'cart' // project name (usually the name of class library module, or any)
            pom. groupId = 'com. hfy. Cart '// unique identification (usually the module package name, or any)

            //Specify the URL of Maven warehouse of snapshot version. Todo, please change it to your own Maven server address, account and password
            snapshotRepository(url: 'http://xxx/maven-snapshots/') {
                authentication(userName: '***', password: '***')
            }
            //Specify the official version of Maven warehouse URL. Todo, please change it to your own Maven server address, account and password
            repository(url: 'http://xxx/maven-releases/') {
                authentication(userName: '***', password: '***')
            }
        }
    }
}

//Type displays the specified task type or task. Here, you specify the task to execute Javadoc, which has been defined in gradle
task androidJavadocs(type: Javadoc) {
    //Set the location of the source code
    source = android.sourceSets.main.java.sourceFiles
}

//Generate Javadoc jar
task androidJavadocsJar(type: Jar) {
    //Specify document name
    classifier = 'javadoc'
    from androidJavadocs.destinationDir
}

//Package the tasks of the code and resources under the main directory to generate sources jar
task androidSourcesJar(type: Jar) {
    classifier = 'sources'
    from android.sourceSets.main.java.sourceFiles
}

//Configure the files that need to be uploaded to Maven warehouse
artifacts {
    archives androidSourcesJar
    archives androidJavadocsJar
}
Copy code

maven_ push. Gradle mainly refers to the configuration of the release component arr: version number, name, Maven warehouse address, account number, etc.

Then, build Referenced in gradle:

//build.gradle
apply from: 'maven_push.gradle'
Copy code

Then, after clicking sync, click the gradle task uploadarchives to package and publish arr to the Maven warehouse.

04 project architecture - componentization - detailed explanation

image.png

Finally, to reference the component arr in the shell project, you need to firstroot directoryDownload build Add Maven warehouse address to gradle:

allprojects {
    repositories {
        google()
        jcenter()
        //Private server warehouse address
        maven {
            url 'http://xxx'
        }
    }
}
Copy code

Then in the app’s build Add dependencies to gradle:

dependencies {
    ...
    implementation 'com.hfy.cart:cart:1.0.0'
    //And other business components
}
Copy code

It can be seen that the multi engineering scheme is the same as the third-party library we usually use, except that we publish the component arr to the company’s private Maven warehouse.

Actually, I’m personallyMulti engineering scheme is recommendedof

  • The single project scheme cannot achieve code permission control, nor can it make the division of responsibilities of developers clear. Each developer can modify any component, which will obviously cause confusion.
  • Multi project divides each component into separate projects, and the code permission can be clearly controlled. During integration testing, you can integrate through Maven reference. In addition, business components and business basic components can also be reused for other projects of the company like basic components.

Note that I use a multi engineering solution in the demo, andSend arr toJitpack warehouse, this is for the convenience of demonstration. It is the same as sending it to the company’s private Maven warehouse. 1. You need build.exe under the root directory Add jitpack warehouse address in gradle: maven {URL ‘https://jitpack.io’} ; 2. Jitpack is a custom Maven repository, but its process is extremely simplified. You only need to enter the GitHub project address to publish the project.

4、 Page Jump

4.1 scheme – arouter

As mentioned earlier, the core of componentization is decoupling, so there can be no dependency between components. So how to realize page jump between components?

For example, clicking the shopping cart button on the home page module needs to jump to the shopping cart page of the shopping cart module. There is no dependency between the two modules, that is to say, you can’t directly use the display startup to open the shopping cart activity. What about the implicit startup? Implicit startup can realize jump, but implicit intent needs to be configured and managed through androidmanifest, so collaborative development is more troublesome. Here, we adopt the common method in the industry –route

The more famous routing framework is Ali’sARouter, meituanWMRouter, their principles are basically the same.

Here we adopt the more widely used arouter: “a framework to help Android App transform into components – supporting routing, communication and decoupling between modules”.

4.2 arouter realizes route jump

As mentioned earlier, all business components rely on common components, so we use keywords in common components“api”Business components can access the added dependencies. If we want to use arouter for interface jump, we need to add arouter dependency to the common component (in addition, the libraries that other components depend on together should also be placed in common for unified dependency).

4.2.1 introducing dependencies

Because arouter is special, the annotationprocessor dependency of “arouter compiler” needs to be added separately to all components used in arouter, otherwise the index file cannot be generated in APT, and the jump cannot succeed. And in the build of each component using arouter In the gradle file, you also need to add a specific configuration in the javacompileoptions in Android {}. Then shell engineering needs to rely on business components. As follows:

//Common component build gradle
dependencies {
    ...
    api 'com.alibaba:arouter-api:1.4.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    //The libraries (network library, picture library, etc.) that business components and basic business components depend on together are written here~
}
Copy code
//Business component build gradle
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
...
}
dependencies {
...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com. github. Hufeiyang: common: 1.0.0 '// business components depend on common components
}
Copy code
//Shell engineering app module build gradle
dependencies {
    ...
    //The private Maven warehouse is not used here, but sent to the jitpack warehouse, which has the same meaning~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com. github. Hufeiyang: Cart: 1.0.1 '// dependent on shopping cart components
    implementation 'com. github. Hufeiyang: Homepage: 1.0.2 '// depends on the home page component

    //The shell project also needs to rely on the common component, because it needs to initialize arouter
    implementation 'com.github.hufeiyang:Common:1.0.0'
}
Copy code

4.2.2 initialization

After the dependency is completed, the arouter must be initialized first, which needs to be completed in the application:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        //These two lines must be written before init, otherwise these configurations will be invalid during init
        if (BuildConfig.DEBUG) {
            //Print log
            ARouter.openLog();
            //Turn on debugging mode (if running in instantrun mode, you must turn on debugging mode! The online version needs to be turned off, otherwise there is a security risk)
            ARouter.openDebug();
        }
        //As early as possible, it is recommended to initialize in the application
        ARouter.init(this);
    }
}
Copy code

4.2.3 route jump

All right, the preparations are finished. And I know that the home page component does not rely on the shopping cart component. Let’s implement the above mentionedHome page component has no dependency, jump to shopping cart component page

Using arouter for simple route jump, there are only two steps: adding annotation path and route jump through path.

1. Add the annotation @ route (path = “/ XX / XX”) on the page that supports routing. Note that the path must have at least two levels, / XX / XX. Here is the cartactivity of the shopping cart component:

@Route(path = "/cart/cartActivity")
public class CartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart);
    }
}
Copy code

2. Then initiate routing operation in homeactivity of the home page component – click the button to jump to the shopping cart and call arouter getInstance(). build(“/xx/xx”). Navigation():

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Route to the shopping cart page of the shopping cart component (but not dependent on the shopping cart component)
                ARouter.getInstance()
                        .build("/cart/cartActivity")
                        . withstring ("key1", "value1") // carry parameter 1
                        . withstring ("key2", "Value2") // carrying parameter 2
                        .navigation();
            }
        });
    }
}
Copy code

In addition, note that notes and paths are added to homeactivity to directly open the home page in the startup page of Shell project. You can also see that route jump can wait for parameters like startactivity.

Finally, in the startup page of the shell project, open the home page through routing (of course, startactivity () can also be used here. After all, the shell project depends on the home page component):

//Start page
public class SplashActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Directly open the homeactivity of the home component through routing,
        ARouter.getInstance().build("/homepage/homeActivity").navigation();
        finish();
    }
}
Copy code

Finally, let’s take a look at the results of the run Shell project:

04 project architecture - componentization - detailed explanation

image.png

Here, the problem of page jump between components has also been solved.

5、 Communication between components

If there is no dependency between components, how can they communicate?

For example, the home page needs to display the quantity of goods in the shopping cart, and the ability to query the quantity of goods in the shopping cart is internal to the shopping cart component. What should I do?

5.1 service exposure components

In normal development, we often decouple the interface and don’t care about the implementation of the interface, so as to avoid the close relationship between interface call and business logic implementation. Here, the decoupling between components is the same idea. It only depends on and invokes the service interface, and will not depend on the implementation of the interface.

You may have a question: since the home page component can access the shopping cart component interface, it needs to rely on the shopping cart component. These two components are still coupled. What should we do? The answer isComponent splitting exposes services。 See the figure below:

04 project architecture - componentization - detailed explanation

image.png

On the left is that components can call each other’s services, but there is dependency coupling. On the right, I found moreexport_homeexport_cart, which is a corresponding split and dedicated to providing servicesExposed components。 The operation instructions are as follows:

  • Exposed components only store service interfaces and entity classes related to service interfaces, routing information, util convenient for service call, etc
  • The service caller only depends on the exposed components of the service provider, such as module_ Home depends on export_ Cart, independent of module_ cart
  • Components need to rely on their own exposed components and implement service interfaces, such as module_ Cart depends on export_ Cart and implement the service interface
  • The implementation injection of the interface is still completed by arouter, use routing information just like page Jump

The following is the implementation of this scheme. The home page calls the shopping cart service to obtain the quantity of goods for better explanation and understanding.

5.2 implementation

5.2.1 new export_ cart

First, create a new module, export, in the shopping cart project_ Cart, create a new interface class icartservice and define the method to obtain the quantity of goods in the shopping cart. Note that the interface must inherit iprovider in order to use the implementation injection of arouter:

/**
 *Exposed services of shopping cart components
 *You must inherit iprovider
 * @author hufeiyang
 */
public interface ICartService extends IProvider {

    /**
     *Get the quantity of goods in the shopping cart
     * @return
     */
    CartInfo getProductCountInCart();
}
Copy code

Cartinfo is the shopping cart information, including the quantity of goods:

/**
 *Shopping cart information

 * @author hufeiyang
 */
public class CartInfo {

    /**
     *Quantity of goods
     */
    public int productCount;
}
Copy code

Next, create the routing table information to store the pages and routing addresses of services provided by the shopping cart component:

/**
 *Shopping cart component routing table
 *That is, the routing information of all pages that can jump from the outside in the shopping cart component
 * @author hufeiyang
 */
public interface CartRouterTable {

    /**
     *Shopping cart page
     */
    String PATH_PAGE_CART = "/cart/cartActivity";

    /**
     *Shopping Cart Service
     */
    String PATH_SERVICE_CART = "/cart/service";

}
Copy code

As mentioned earlier, the path string is directly used for route jump during page Jump. Here, both and service routes are managed here.

Then, for the convenience of external components, create a new cartserviceutil:

/**
 *Shopping cart component service tool class
 *Other components can directly use this class: page Jump and obtain services.
 * @author hufeiyang
 */
public class CartServiceUtil {

    /**
     *Jump to shopping cart page
     * @param param1
     * @param param2
     */
    public static void navigateCartPage(String param1, String param2){
        ARouter.getInstance()
                .build(CartRouterTable.PATH_PAGE_CART)
                .withString("key1",param1)
                .withString("key2",param2)
                .navigation();
    }

    /**
     *Get service
     * @return
     */
    public static ICartService getService(){
        //return ARouter. getInstance(). navigation(ICartService.class);// If there is only one implementation, this method can also be used
        return (ICartService) ARouter.getInstance().build(CartRouterTable.PATH_SERVICE_CART).navigation();
    }

    /**
     *Get the quantity of goods in the shopping cart
     * @return
     */
    public static CartInfo getCartProductCount(){
        return getService().getProductCountInCart();
    }
}
Copy code

Note that static methods are used here to provide page Jump, service acquisition and service specific method acquisition respectively. Service acquisition and page Jump also use routing, and the service interface implementation class also needs to add @ route annotation to specify the path.

Here, export_ Cart is ready. We also release an export_ Cart’s arr (“com. GitHub. Hufeiyang. Cart: export_cart: XXX”).

Let’s look at module_ Cart implementation of service interface.

5.2.2 module_ Implementation of cart

First, module_ Cart needs to rely on export_ cart:

//module_ Cart's build gradle
dependencies {
    ...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'

    //Dependent export_ cart
    implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5'
}
Copy code

Click sync, and then change the path of cartactivity to the routing table to provide:

@Route(path = CartRouterTable.PATH_PAGE_CART)
public class CartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart);
    }
}
Copy code

Then, create an implementation class of the service interface to implement icartservice,Add @ route annotation to specify the service route defined in cartroutertable

/**
 *Implementation of shopping cart component service
 *The @ route annotation is required to specify the service route defined in the cartroutertable
 * @author hufeiyang
 */
@Route(path = CartRouterTable.PATH_SERVICE_CART)
public class CartServiceImpl implements ICartService {

    @Override
    public CartInfo getProductCountInCart() {
        //Here, the actual project should be to request the interface or query the database
        CartInfo cartInfo = new CartInfo();
        cartInfo.productCount = 666;
        return cartInfo;
    }

    @Override
    public void init(Context context) {
        //The initialization work will be called during service injection, which can be ignored
    }
}
Copy code

The implementation here is to directly instantiate cartinfo and assign a quantity of 666. Then publish an arr (“com. GitHub. Hufeiyang. Cart: module_cart: XXX”).

5.2.3 module_ Use and debugging in home

module_ Home needs to rely on export_ cart:

//module_ Home build gradle
dependencies {
    ...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'

    //Note that only export is relied on here_ Cart (module_cart introduced by shell Engineering)
    implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5'
}
Copy code

Add a textview in homeactivity and call cartserviceutil to obtain and display the number of goods in the shopping cart:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        //Jump to shopping cart page
        findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //Route to the shopping cart page of the shopping cart component (but not dependent on the shopping cart component)
//                ARouter.getInstance()
//                        .build("/cart/cartActivity")
//                        . Withstring ("key1", "Param1") // carrying parameter 1
//                        . Withstring ("key2", "param2") // parameter 2 is carried
//                        .navigation();

                CartServiceUtil.navigateCartPage("param1", "param1");
            }
        });

        //Call shopping cart component service: get the quantity of goods in shopping cart
        TextView tvCartProductCount = findViewById(R.id.tv_cart_product_count);
        tvCartProductCount. Settext ("number of goods in shopping cart:" + cartserviceutil. Getcartproductcount() productCount);
    }
}
Copy code

See using cartserviceutil Getcartproductcount() gets the shopping cart information and displays it. The jump page is also changed to cartserviceutil Navigatepartpage() method.

Go to the home component hereIndependent debuggingWhat’s more: page Jump and service call, independent debugging OK, and then integrated into the shell project. First, let the app module of the homepage project rely on common components and modules_ Cart andLocal module_ home

//Homepage project, build of APP module gradle
dependencies {
    ...
    //Introduce local common components and modules_ cart、module_ Home, which can be debugged and used independently in app module
    implementation 'com.github.hufeiyang:Common:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6'

    implementation project(path: ':module_home')
}
Copy code

Then create a new myapplication, initialize arouter, and use arouter in the mainactivity of the app getInstance(). build(“/homepage/homeActivity”). Navigation () opens the home page so that you can debug.

After debugging OK, it is then integrated into the shell project.

5.2.4 integration into shell Engineering

The operation in shell engineering is similar to independent debugging, except that arr is introduced into the home page components:

dependencies {
    ...
    //The private Maven warehouse is not used here, but sent to the jitpack warehouse, which has the same meaning~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6'
    implementation 'com.github.hufeiyang:HomePage:1.0.4'

    //The shell project also needs to rely on the common component, because it needs to initialize arouter
    implementation 'com.github.hufeiyang:Common:1.0.0'
}
Copy code

Finally, run the shell project to see the following results:

04 project architecture - componentization - detailed explanation

image.png

The number obtained is 666. The page Jump is successful.

In addition, in addition to export_ XXX in this way, you can also add a componentbase component, which is dependent on all common components. In this component, you can add services that define abstract methods that business components can provide external access to their own data. It is equivalent to integrating the export of various business components into componentbase, so only one component is added. However, this is not easy to manage. The change of external capability of each component must be changed to componentbase.

In addition, in addition to the component roomMethod call, useEventbus transfers information between componentsIt is also OK (note that the event entity class should be defined in export_xxx).

Well, here, the communication problem between components has also been solved.

6、 Fragment instance acquisition

The jump of activity is introduced above, and we will often use fragment. For example, homeactivity, a common application homepage, contains multiple fragments belonging to different components, or one fragment needs to be used by multiple components. Usually, we directly access the specific fragment class to create a new fragment instance, but there is no direct dependency between components here. What should we do? The answer is still yesARouter

First in module_ Create cartfragment in cart:

//Add annotation @ route and specify the path
@Route(path = CartRouterTable.PATH_FRAGMENT_CART)
public class CartFragment extends Fragment {
    ...
    public CartFragment() {
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        //Display "cart_fragment"“
        return inflater.inflate(R.layout.fragment_cart, container, false);
    }
}
Copy code

At the same timeFragment add the annotation @ route to specify the route path, the route is still defined in export_ Cartroutertable of cart, so export_ Cart needs to send an arr module first_ Cart to rely on, and then module_ Cart releases arr.

Then module_ Depend on export in home_ Cart, use arouter to obtain fragment instance:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        ...
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction= manager.beginTransaction();

        //Use arouter to get the fragment instance and add it
        Fragment userFragment = (Fragment) ARouter.getInstance().build(CartRouterTable.PATH_FRAGMENT_CART).navigation();
        transaction.add(R.id.fl_test_fragment, userFragment, "tag");
        transaction.commit();
    }
}
Copy code

It can be debugged independently and then integrated into the shell project — relying on the latest module_ Cart, homepage, the results are as follows:

04 project architecture - componentization - detailed explanation

image.png

The green part is the fragment referenced from the cart component.

7、 Application lifecycle distribution

We usually do some initialization tasks in oncreate of application, such as arouter initialization mentioned earlier. Business components sometimes need to obtain the application of the application, and some initialization tasks should be carried out when the application is started.

You might say that the oncreate operation of the shell project application is OK. But doing so brings problems: because we want shell engineering and business componentsCode isolation(although there are dependencies), and we want the tasks inside the component to be completed inside the business component.

So how to achieve each business componentNon intrusive access to the application lifecycleAnd—— The answer is to useApplifecycle plug-in, it is specially used to actively distribute the application life cycle to components in Android component development. The specific use is as follows:

  1. The common component relies on the applicycle API

First, the common component adds the applicycle API dependency through the API and publishes the ARR:

//Common component build gradle
dependencies {
    ...
    //AppLifecycle
    api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
}
Copy code
  1. Business components rely on applicycle compiler, implementation interface and annotation

Each business component should rely on the latest common component, and add the dependency of applicycle compiler:

//Business component build gradle
...
    //Here, common: 1.0.2 relies on the applicycle API
    implementation 'com.github.hufeiyang:Common:1.0.2'
    annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
Copy code

After sync, create a new class to implement the interfaceIApplicationLifecycleCallbacksUsed to receive the application lifecycle and [email protected]AppLifecycleNotes.

For example, the implementation of cart component:

/**
 *Applifecycle of components
 * 1、@AppLifecycle
 *2. Implement iapplicationlifecycle callbacks
 * @author hufeiyang
 */
@AppLifecycle
public class CartApplication implements IApplicationLifecycleCallbacks {

    public  Context context;

    /**
      *Used to set the priority, that is, the priority of the oncreate method calls of multiple components
      * @return
     */
    @Override
    public int getPriority() {
        return NORM_PRIORITY;
    }

    @Override
    public void onCreate(Context context) {
        //The initialization task can be done here, which is equivalent to the oncreate method of application
        this.context = context;

        Log.i("CartApplication", "onCreate");
    }

    @Override
    public void onTerminate() {
    }

    @Override
    public void onLowMemory() {
    }

    @Override
    public void onTrimMemory(int level) {
    }
}
Copy code

The implementation methods include oncreate, onterminate, onlowmemory and ontrimmemory. The most important thing isOncreate method, which is equivalent to oncreate method of application. You can do initialization here。 In addition, you can set the priority of calling oncreate method of callback multiple components through getpriority() method. There is no special requirement to set norm_ Priority is enough.

  1. Shell engineering introduces applifecycle plug-in and triggers callback

Shell engineering introduces new common components, business components, and applifecycle plug-ins:

//Shell project root directory build gradle

buildscript {

    repositories {
        google()
        jcenter()

        //The applicycle plug-in repository is also a jitpack
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'

        //Load plug-in applicycle
        classpath 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-plugin:1.0.3'
    }
}
Copy code
//Build. Of APP module gradle

apply plugin: 'com.android.application'
//Using the plug-in applicycle
apply plugin: 'com.hm.plugin.lifecycle'
...
dependencies {
    ...
    //The private Maven warehouse is not used here, but sent to the jitpack warehouse, which has the same meaning~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.11'
    implementation 'com.github.hufeiyang:HomePage:1.0.5'

    //Common components also need to be relied on in the shell project, because life cycle distribution needs to be triggered
    implementation 'com.github.hufeiyang:Common:1.0.2'
}
Copy code

Finally, you need to trigger the distribution of the life cycle in the application:

//Shell engineering myapplication
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ...

        ApplicationLifecycleManager.init();
        ApplicationLifecycleManager.onCreate(this);
    }

    @Override
    public void onTerminate() {
        super.onTerminate();

        ApplicationLifecycleManager.onTerminate();
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();

        ApplicationLifecycleManager.onLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);

        ApplicationLifecycleManager.onTrimMemory(level);
    }
}
Copy code

First, call the init () method of applicationlifcyclemanager in the increate method to collect the classes in the component that implement iaapplicationlifcyclecallbacks and add the @applifecycle annotation. Then, call the corresponding application lifecycle manager method within each lifecycle method to distribute it to all components.

In this way, the component can receive the life cycle of the application.If you add a new component, you only need to implement iaapplicationlifcyclecallbacks and add @ applifecycle annotation. There is no need to modify the shell project or care about it

Applifecycle plug-in uses apt technology, gradle plug-in technology + ASM to dynamically generate bytecode. Most of the work has been completed in the compilation stage, which is asexual and easy to use.

So far, the five problems of component-based development have been solved. Let’s take a look at how to realize component transformation for old projects.

8、 Componentization of old projects

Usually, we do componentization in order to transform existing old projects. It may be that the coupling between modules within the old project is serious, there is no strict division of business modules, and component transformation is a heavy workload, and full regression test is required. Generally speaking, it requires the participation of all staff and is more difficult.

8.1 scheme

8.1.1 component Division

According to the component architecture diagram introduced earlier, components are divided into basic components, business basic components and business components.

  • Basic components, needless to say, are basic functions, such as network request, log framework and image loading. These have nothing to do with business and can be used for all projects of the company. They are the most stable components at the bottom. It is easier to identify and split here.
  • Business basic components are mainly used by business components, such as sharing and payment components. They are usually a complete function and the most stable component. This part is usually easy to identify.
  • Business components, complete business blocks, such as “home page”, “classification”, “discovery”, “shopping cart” and “my” mentioned above. Business components are the main battlefield of daily requirements development.

8.1.2 component splitting: basic component and common component

The basic component is the easiest to split, with the least dependence and single and pure function. Extract the things that the basic components depend on from the old project, put them in a separate project, make them into separate components, and release the ARR to the Maven warehouse of the company. Note that there must be no business related codes.

Create a new common component and use “API” to rely on all basic components, so that components that rely on common components can use the functions of all basic components. Then, there are the dependencies of arouter, applifecycle and other third-party libraries mentioned earlier.

In addition, there is another important part of the common component: providing baseactivity and basefragment. Here, base needs to add basic capabilities, such as page entry and exit buried point reporting, unifying page title style, opening and closing eventbus, etc.

8.1.3 component splitting: business basic component and business component

Basic business components basically only rely on common, and their functions are single and pure. The same is to extract the dependent things, put them in a separate project, make separate components, and release arr to the Maven warehouse of the company.

For business components, first identify the boundary of the component, which can be judged according to the page entry and exit. Then, we need to identify the dependence on the basic business components; And, most importantly, dependence on other business components. Can firstSeparate the code from a separate project, and then rely on common components and required business basic components. At this time, the place that still reports an error is the dependence on other business components. At this time, you can put forward requirements to the person in charge of the corresponding component in export_ Jump and service are provided in XXX. Then you just need to rely on export_ XXX can be used

The modular transformation of old projects needs to be carried out step by step, unless there is a special time. Generally, requirements development and transformation go hand in hand. It is easier to complete a component first, then have experience, and then implement other business components one after another.

8.2 frequently asked questions

8.2.1 butterknife error in component – R2

In the library, the use of r.id in butterknife annotation will report an error, such as common component module_ Create a new activity in common and rely on butterknife:

android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.3'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
Copy code

Errors are reported as follows:

04 project architecture - componentization - detailed explanation

image.png

resolvent: you need to add the butterknife plug-in, and then use R2:

buildscript {
  repositories {
    mavenCentral()
    google()
  }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
  }
}
Copy code
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
Copy code

Then it’s OK to use R2 in butterknife annotation:

04 project architecture - componentization - detailed explanation

image.png

Here, all the knowledge of Android componentization is finished.

8、 Summary

This paper introduces the background, architecture, advantages, problems to be solved and detailed solutions of component-based development, such as independent debugging, page Jump, component communication, and finally introduces the component-based scheme of old projects.

The most important tool involved is arouter, which is specially used for Android component decoupling. There are many advanced uses of arouter. I also wrote a comprehensive analysis of arouter when I had the opportunity. Another important knowledge point is applifecycle plug-in. Its principle involves apt, ASM bytecode insertion, gradle plug-in and other technologies. This knowledge will be analyzed later.

Android development componentization is a technology that must be used after the project has developed to a certain scale. It is very necessary to learn to fully master it.

OK, that’s all for today. Welcome to leave a message for discussion~

Reference and thanks:
“Android componentization, from getting started to being unable to extricate oneself”
Android componentization best practices
Android component development practice series