This article is sponsored by Yugangshuo writing platform

Original author: Ren Xuelong

Copyright: this article belongs to the wechat public account Yu Gang said all, without permission, shall not be reproduced in any form

The Demo address: https://github.com/renxuelong/ComponentDemo

The demo first

In the development process of the project, with the increase of the number of developers and the increase of functions, if a reasonable development architecture is not used in advance, the code will become more and more bloated and the code coupling between functions will become more and more serious. At this time, in order to ensure the quality of the project code, we must refactor.

Relatively simple development architecture is divided according to functional modules, that is, using the concept of Module in Android development, each function is a module, and the code of each function is added in its own module. Such a design makes sense when the functions are directly independent of each other, but the code coupling increases when the same functions are involved in multiple modules.

For example, the home page module and broadcast module may involve the function of video playback, at this time, no matter the play control code to the home page or broadcast room, the development process will find that we want to solve the code coupling situation again and again. To further solve this problem, the componentized development model came along.

First, the difference between componentization and modularization

What are the boundaries and what are the essential differences between modularity and componentization? To solve these problems, we need to understand the difference between “modules” and “components.”

The module

Module refers to the independent business module, such as the [home page module], [broadcast room module] and so on.

component

Component refers to a single functional component, such as , [payment component], etc., each component can be developed as a separate module, and can be extracted separately as an SDK for external use.

From this point of view, the most obvious difference between [module] and [component] is that a module is more granular than a component. A module may contain multiple components. And the underlying idea is the same, both for code reuse and business decoupling. When it comes to partitioning, modularity is business-oriented and componentization is function-oriented.

Above is a very basic componentized architecture diagram with the application layer, component layer, and base layer from top to bottom.

Base layer: Base layer is easy to understand, which contains some basic libraries and encapsulation of basic library, such as commonly used pictures of loading, the network request, data storage operation, etc., other modules or components can refer to the same set of basic library, such not only only need to develop a set of code, decoupling the basis functions and also business function coupling, in basic library changes easier operation.

Component layer: Above the base layer is the component layer, which contains simple functional components such as video, payments, and so on

Is the application layer, application layer, component layer up here for the sake of simplicity, only added an APP, the APP is equivalent to our module, a specific business module according to the need to refer to different components, finally realizes the business functions, here again if multiple business module, can respectively according to the need to reference the components, the final output APP will each module as a whole.

At this point, we can use the simplest componentized architecture, but this is only the ideal state of the architecture, in the actual development, different components can not be completely isolated from each other, there will be components to pass data to each other, call methods, page jump, and so on.

For example, in the live broadcast component, users need to brush gifts, which requires the support of the payment component, while in the payment component, the payment operation must require login status, user ID and other information. If you are not logged in, you need to log in to the login component first. The payment process can be normal only after the login is successful.

In our architecture diagram above, the components are isolated from each other, and there is no interdependence. If you want to interact directly with the components, that is, the components depend on each other, this violates the rules of componentized development. So we have to find a way to solve these problems in order to do componentized development.

Ii. Problems to be solved in componentized development

In the process of realizing componentization, there may be different technical paths to solve the same problem, but the main problems to be solved are as follows:

  1. Each component is an integral whole, so it is important to run and debug each component separately during development, which can also speed up the compilation of the project during development.

  2. Data passing and methods calling each other between components is also one of the issues we mentioned above that must be addressed.

  3. Interface hopping between components. Different components can not only transfer data, but also hop to each other. How to achieve jump to each other without interdependence in the componentized development process?

  4. How do I get an instance of a Fragment in a component and add the Fragment instance to the main project interface without directly accessing a specific class in the component?

  5. How to achieve the integration debugging between components after the completion of development? Also, during the integration debugging phase, when the development depends on multiple components, if the implementation depends on only a few components, can the compilation pass? This also reduces compilation time and improves efficiency.

  6. The goal of component decoupling and how to achieve code isolation? Not only are components isolated from each other, but the fifth problem is that modules can dynamically add and delete components when they depend on components. In this way, modules do not operate on specific classes in components, so completely isolated modules use classes in components to make decoupling more complete and the program more robust.

These are the main problems we need to solve in the process of realizing componentization. Next, we will solve one by one, and finally achieve a more reasonable componentization development.

Debug components separately

1. What is the type of project for dynamically configuring components?

AndroidStudio builds Android projects using Gradle. To be specific, Android Gradle plug-in is used to build. Android Gradle provides three plug-ins. In development, you can configure different projects by configuring different plug-ins.

  • App plug-in, ID: com.android.application
  • Library plugin, id: com.android.libraay
  • Test plugin, id: com.android.test

The difference is simple: the App plugin configures an Android App project and outputs an APK installation package after the project is built. The Library plugin configures an Android Library project and outputs an AAR package after the project is built. The Test plugin to configure an Android Test project. We mainly use the App plug-in and the Library plug-in to implement the component debugging separately. Here comes the first small question, how do you dynamically configure the engineering type of a component?

The Android Gradle plugin ID is used to configure the type of the project. However, our components can be independently debuggable and can be relied on by other modules, so we should not write the plugin ID here. Instead, add a gradle.properties configuration file to the Module, and add a Boolean variable isRunAlone to the configuration file, Use the isRunAlone value in build.gradle to configure different project types by using different plug-ins. You can modify the isRunAlone value directly during individual and integration debugging. For example, the configuration in the Share component:

2. How to dynamically configure the ApplicationId and AndroidManifest file of a component

In addition to configuring different projects by relying on plug-ins, we also need to modify other configurations based on the value of isRunAlone. An APP has only one ApplicationId, so components should have different Applicationids for individual and integrated debugging. Generally speaking, an APP should have only one launch page, which is also required when the component is debugged separately. If the problem of launch page is not dealt with during integration debugging, there will be two launch pages after the Main project and the Component’s AndroidManifes files are merged. This problem also needs to be solved.

ApplicationId and AndroidManifest files can be configured in the build.gradle file, So we also dynamically modify ApplicationId and AndroidManifest using the value of the isRunAlone variable defined when dynamically configuring the component project type. First of all, we need to create a new AndroidManifest.xml file, plus the original AndroidManifest file, in the two files can be configured to separate debugging and integration of different configurations, as shown in the figure:

The contents of the AndroidManifest file are as follows:

/ / main/manifest/AndroidManifest. XML separate debugging <? xml version="1.0" encoding="utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.loong.share">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ShareActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> </ main/ Androidmanifest.xml integration debugging <? xml version="1.0" encoding="utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.loong.share">

    <application android:theme="@style/AppTheme">
        <activity android:name=".ShareActivity"/>
    </application>

</manifest>
Copy the code

Then, in build.gradle, you can configure different paths for ApplicationId and Androidmanifest.xml files by determining the value of isRunAlone:

// Build. Gradle Android {defaultConfig {if(isrunalone.toboolean ()) {// Add applicationId when debugging alone, remove applicationId when debugging with integration"com.loong.login"}... }sourceSets {main {// Separate debugging and integration debugging use a different androidmanifest.xml fileif (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'}}}}Copy the code

Here we have solved the first problem of componentized development by implementing separate and integrated debugging of components and different configurations for different situations. Of course, build. Gradle through Android gradle plug-in, we can also according to different projects to configure different Java source code, different resource resource files, and so on, with the above problem solution, these problems can be solved.

Data transfer and method invocation between components

Since the main project and components, components and components can not directly use the mutual reference of classes to carry out data transfer, so in the development process, if there is data transfer between components, how to solve, here we can adopt the way of [interface + implementation] to solve.

Here you can add a ComponentBase module, which all components depend on, and add services that define the abstract methods that components can use to access their data. Each component provides a class that implements the abstract methods in its corresponding Service. After the component loads, you need to create an object that implements the class, and then add the object that implements the class of Service to the ServiceFactory. In this way, when different components interact, the ServiceFactory can get the interface implementation of the component that you want to call, and then call the specific method in it to achieve data transfer and method invocation between components.

Of course, the ServiceFactory also provides a null implementation for all services to avoid nullpointer exceptions when components are debugged individually or as part of an integration.

Let’s follow this approach to solve the problem of data transfer between components and method calls to each other, here we demonstrate by sharing a scenario in which a component calls a method in a login component to get the login state.

1. Create the ComponentBase module

AndroidStudio Module creation is relatively simple, through the menu bar File -> New -> New Module to create our componentBase Module. Note that we need to use Phone & Tablet Module to create components, and Android Library to create componentBase. The difference is that the default APP project is created with the Phone & Tablet Module, and the default Library project is created with the Android Library. Of course, if you choose the wrong one, you can also modify the configuration in Buidl. gradle. The diagram below:

The Login component provides two methods to obtain the Login status and the Login user AccountId. The share component requires the user to be logged in to perform the share operation. If the user is not logged in, the share operation will not be performed. Let’s take a look at the structure of the componentBase module:

The service folder defines the interface. The IAccountService interface defines the interface method for transferring data provided by the Login component. The empty_service folder is the empty implementation of the interface defined in the service. The ServiceFactory receives the registration of interface objects implemented in a component and provides the interface implementation for a particular component.

// IAccountService public interface IAccountService {/** * Whether you have logged in * @return*/ boolean isLogin(); /** * Get AccountId * @ for the login userreturn
     */
    String getAccountId();
}

// EmptyAccountService
public class EmptyAccountService implements IAccountService {
    @Override
    public boolean isLogin() {
        return false;
    }

    @Override
    public String getAccountId() {
        returnnull; } } // ServiceFacoty public class ServiceFactory { private IAccountService accountService; /** * Disable external creation of ServiceFactory objects */ privateServiceFactory() {} /** * Public static ServiceFactory singleton */ Public static ServiceFactorygetInstance() {
        returnInner.serviceFactory; } private static class Inner { private static ServiceFactory serviceFactory = new ServiceFactory(); } /** * Receive a Service instance implemented by the Login component */ public voidsetAccountService(IAccountService accountService) { this.accountService = accountService; } /** * Return the Service instance of the Login component */ public accountServicegetAccountService() {
        if (accountService == null) {
            accountService = new EmptyAccountService();
        }
        returnaccountService; }}Copy the code

In the componentize architecture diagram we mentioned earlier, all components depend on the Base module, and the ComponentBase module is also dependent on all components, so we can make the Base module depend on the ComponentBase module. In this way, you can access the classes in the ComponentBase module after relying on the Base module in the component.

2. The Login component registers interface objects in the ServiceFactory

After componentBase defines services for the Login component, the Login component relies on the ComponentBase module. Then create a class in the Login component that implements the IAccountService interface and its interface methods. And register the implementation class object of the IAccountService interface with the ServiceFactory when the Login component is initialized (preferably in the Application). The relevant code is as follows:

// Base module build.gradle dependencies {API project (':componentbase')... } // login component build.gradle dependencies {implementation fileTree(dir:'libs', include: ['*.jar'])
    implementation project (':base'Public class AccountService implements IAccountService {@override public Boolean AccountService implements IAccountService {@override public BooleanisLogin() {
        returnAccountUtils.userInfo ! = null; } @Override public StringgetAccountId() {
        returnAccountUtils.userInfo == null ? null : AccountUtils.userInfo.getAccountId(); }} // Aplication in login component public Class LoginApp extends BaseApp {@Override public voidonCreate() { super.onCreate(); / / the AccountService instances of the class registration to the ServiceFactory ServiceFactory. GetInstance (). SetAccountService (new AccountService ()); }}Copy the code

The above code is the key code in the Login component to provide services externally. Some friends here may think that a project can only have one Application. When Login is used as a component, the Application class of the main module will be initialized. Applicaiton in the Login component is not initialized. This is indeed a problem. Let’s put the registration of the Service here, and later we will solve the problem that Appliaciton does not initialize when Login is a component.

3. Data transfer between Share component and Login component

After the Login component registers the IAccountService implementation object in the ServiceFactory, other modules can use the Service for data transfer with the Login component. We need to use the Login status in the Share component. Next, let’s look at how the Share component uses the Service provided by the Login component.

The Share component also relies on the Base module, so you can directly access the classes in the ComponentBase module. In the Share component, use getAccountService of the ServiceFactory object to obtain the IAccountService interface implementation object provided by the Login component. Data transfer with the Login component can then be achieved by calling the methods of the object. The main code is as follows:

// buidl.gradle dependencies {implementation project (':base')... } // Share component ShareActivity public class ShareActivity extends AppCompatActivity {@override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);setContentView(R.layout.activity_share);

        share();
    }

    private void share() {
        if(ServiceFactory.getInstance().getAccountService().isLogin()) {
            Toast.makeText(this, "Sharing success", Toast.LENGTH_SHORT);
        } else {
            Toast.makeText(this, "Share failed: user not logged in", Toast.LENGTH_SHORT); }}}Copy the code

This development pattern realizes that the data transfer between each component is based on the interface programming, the interface and the implementation are completely separated, so the decoupling between components is realized. In the more extreme case where the implementation class inside a component modifies the implementation of a method, we simply delete and replace the component, as long as the new component implements the abstract method in the corresponding Service and registers the implementation class object in the ServiceFactory at initialization. None of the other components with which this component passes data need to be modified.

There are many other ways to interact with components, such as EventBus, broadcast, data persistence, etc., but they are often less intuitive. So for the interactions that can be achieved through services, We’d better do it this way.

4. Dynamic configuration of the Application component

The above mentioned problem is that due to the Application substitution principle, the component’s Applicaiton will not be initialized when debugging centrally if the main module has Application, etc. The registration of our component’s Service in the ServiceFactory must be placed where the component was initialized.

To solve this problem, we can initialize the component’s Service class with a strong reference to the main Module’s Application, which requires the main Module to have direct access to the component’s classes. Since we don’t want the main module to have access to the component’s classes during development, we can use reflection to initialize the component’s Application.

1) Step 1: Define the abstract class BaseApp in the Base module to inherit Application, which defines two methods. InitModeApp is the method to be called when initializing the current component, and initModuleData is the method to be called after all components are initialized.

Public abstract class BaseApp extends Application {/** * Application initialization */ public abstract void initModuleApp(Application application); /** * All custom operations after Application initialization */ public abstract void initModuleData(Application Application); }Copy the code

2) Step 2: All components’ applications inherit BaseApp and implement operations in corresponding methods. Let’s take the Login component for example, whose LoginApp implements the BaseApp interface. The initModuleApp method completes registering your Service object in the ServiceFactory. The onCreate() method also calls the initModuleApp() method to complete registration in the ServiceFactory when debugging separately.

Public class LoginApp extends BaseApp {@override public voidonCreate() {
        super.onCreate();
        initModuleApp(this);
        initModuleData(this);
    }

    @Override
    public void initModuleApp(Application application) {
        ServiceFactory.getInstance().setAccountService(new AccountService());
    }

    @Override
    public void initModuleData(Application application) {

    }
}
Copy the code

3) Step 3: Define the AppConfig class in the Base module, where moduleApps is a static String array into which we put the full Application class names of the components that need to be initialized.

Public class AppConfig {private static final String LoginApp ="com.loong.login.LoginApp";

    public static String[] moduleApps = {
            LoginApp
    };
}
Copy the code

4) Step 4: The Application of the main Module also inherits BaseApp and implements two initialization methods that iterate over the class names in the moduleApps array defined in class AppcConfig. Through reflection, Initialize the Application for each component.

Applicaiton Public Class MainApplication extends BaseApp {@Override public voidonCreate() { super.onCreate(); // Initialize the component. Application initModuleApp(this); // Other operations // initModuleData(this) after all Application initialization; } @Override public void initModuleApp(Application application) {for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BaseApp baseApp = (BaseApp) clazz.newInstance();
                baseApp.initModuleApp(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void initModuleData(Application application) {
        for(String moduleApp : AppConfig.moduleApps) { try { Class clazz = Class.forName(moduleApp); BaseApp baseApp = (BaseApp) clazz.newInstance(); baseApp.initModuleData(this); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); }}}}Copy the code

At this point, we have completed the initialization operation of the component Application through reflection, and also realized the decoupling requirement in the component and transformation.

4. The interface between components jumps

In Android, there are two types of interface jumps: explicit Intent and implicit Intent. In the same component, because classes are freely accessible, interface jumps can be implemented with explicit intents. In componentized development, because different components do not depend on each other, they cannot directly access each other’s classes, so there is no way to do this in an explicit way.

The implicit Intent method provided in Android can fulfill this requirement, but implicit Intent needs to be centrally managed through the Android manifest, which makes collaborative development more troublesome. So here we take a more flexible way, using Alibaba open source ARouter to achieve.

A framework for helping Android apps with componentization — routing, communication, decoupling between modules

As ARouter’s introduction on Github shows, it enables routing between components. Routing refers to the process of routing data packets received from one interface to another interface based on the destination address of the data packets. This can reflect the characteristics of the way by jump, very suitable for component decoupling.

To use ARouter to jump to the interface, we need our component to add a dependency on ARouter. Since all components depend on the Base module, we can simply add a dependency on ARouter in the Base module. Libraries that other components depend on together are also best placed in Base.

It is important to note that the dependency on aroutter-compiler needs to be added separately to all modules and components that use arouter, otherwise the index file cannot be generated in APT and the jump cannot succeed. For each build.gradle file that uses ARouter’s module and component, the javaCompileOptions in its Android {} file also needs to be configured.

// Base module build.gradle dependencies {API'com. Alibaba: arouter - API: 1.3.1'// Aroutter-compiler annotation dependencies require all modules using arouter to add a dependency on annotationProcessor'com. Alibaba: arouter - compiler: 1.1.4'
}
Copy the code
// Build. Gradle android {defaultConfig {... javaCompileOptions { annotationProcessorOptions { arguments = [ moduleName : project.getName() ] } } } } dependencies { ... implementation project (':base')
    annotationProcessor 'com. Alibaba: arouter - compiler: 1.1.4'
}
Copy the code
// Add dependencies to build.gradle for login and share components. Other implementation project (':login')
    implementation project(':share')}Copy the code

Now that we have added a dependency on ARouter, we need to initialize ARouter in the Application of the project. In this case, we put the initialization of ARouter in the onCreate method of the main project Application. Initialize ARouter at the same time as the application starts.

Public class MainApplication extends Application {@override public voidonCreate() { super.onCreate(); // Initialize ARouterif(isDebug()) {// these two lines must be written before init, otherwise these configurations will be invalid during init. // Enable debug mode (if running in InstantRun mode, you must enable debug mode! Online versions need to be closed, otherwise there is a security risk) arouter.opendebug (); } // Initialize ARouter ARouter. Init (this); // Other operations... } private booleanisDebug() {
        returnBuildConfig.DEBUG; } // Other code... }Copy the code

Here we take the main project to skip the login interface, and then the login interface to skip the sharing interface of the share component after the successful login as an example. The sharing function also uses the Service we mentioned above that calls the login component to determine the login status.

First, you need to add LoginActivity and ShareActivity in the login and share components, and then annotate the two activities with a Route, where path is the path to jump to. Note that the path should have at least two levels, /xx/xx

The LoginActivity of the Login component:

@Route(path = "/account/login")
public class LoginActivity extends AppCompatActivity {

    private TextView tvState;

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

        initView();
        updateLoginState();
    }

    private void initView() {
        tvState = (TextView) findViewById(R.id.tv_login_state);
    }

    public void login(View view) {
        AccountUtils.userInfo = new UserInfo("10086"."Admin");
        updateLoginState();
    }

    private void updateLoginState() {
        tvState.setText("Here is the login screen:" + (AccountUtils.userInfo == null ? "Not logged in" : AccountUtils.userInfo.getUserName()));
    }

    public void exit(View view) {
        AccountUtils.userInfo = null;
        updateLoginState();
    }

    public void loginShare(View view) {
        ARouter.getInstance().build("/share/share").withString("share_content"."Sharing data to Twitter.").navigation(); }}Copy the code

ShareActivity of the Share component:

@Route(path = "/share/share")
public class ShareActivity extends AppCompatActivity {
    private TextView tvState;
    private Button btnLogin, btnExit;

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

        initView();
        updateLoginState();
    }

    private void initView() {
        tvState = (TextView) findViewById(R.id.tv_login_state);
    }

    public void login(View view) {
        AccountUtils.userInfo = new UserInfo("10086"."Admin");
        updateLoginState();
    }

    public void exit(View view) {
        AccountUtils.userInfo = null;
        updateLoginState();
    }

    public void loginShare(View view) {
        ARouter.getInstance().build("/share/share").withString("share_content"."Sharing data to Twitter.").navigation();
    }
    
    private void updateLoginState() {
        tvState.setText("Here is the login screen:" + (AccountUtils.userInfo == null ? "Not logged in": AccountUtils.userInfo.getUserName())); }}Copy the code

Then in the MainActivity, enter the ARouter, where build is the path address, withXXX is the key and value of the parameters carried by the Activity jump, and navigation is the route jump.

Public Class MainActivity extends AppCompatActivity {@override protected void onCreate(@nullable) Bundle savedInstanceState) { super.onCreate(savedInstanceState);setContentView(R.layout.activity_main); } public void login(view view){arouter.getinstance ().build();"/account/login").navigation(); } public void share(view view){arouter.getInstance ().build();"/share/share").withString("share_content"."Sharing data to Twitter.").navigation(); }}Copy the code

Those of you who have studied the source code for ARouter may know that ARouter has its own compile-time annotation framework, and that the jump is done by the helper classes generated at compile time, and that the final implementation actually calls startActivity.

Another important role of routing is filtering and intercepting. Take ARouter as an example, if we define a filter, all filters will be traversed before the module jump, and then the jump path will be judged to find the jump that needs to be intercepted. For example, the sharing function mentioned above generally requires user login. If we don’t want to add logon status to all shared places, we can use the route filtering function. Let’s demonstrate this function. We can define a simple filter:

@interceptor (priority = 8, name ="Login Status Interceptor") public class LoginInterceptor implements IInterceptor { private Context context; @override public void process(Postcard Postcard, InterceptorCallback callback) {// onContinue and onInterrupt need to call at least one of them, Otherwise, the route will not continueif (postcard.getPath().equals("/share/share")) {
            if(ServiceFactory.getInstance().getAccountService().isLogin()) { callback.onContinue(postcard); // Return control when processing is complete}else {
                callback.onInterrupt(new RuntimeException("Please log in")); // Interrupt the routing process}}else{ callback.onContinue(postcard); }} @override public void init(Context Context) {// Override public void init(Context Context) { This. Context = context; }}Copy the code

Custom filters need to be annotated with @tnterceptor, priority is the priority and name is the description of the interceptor. The code above uses the Postcard to get the jump’s path, and then uses the path and specific requirements to determine whether to intercept, in this case by judging the login status, continuing the jump if logged in, or intercepting the jump if not logged in.

How can the main project use a component’s Fragment without directly accessing the component’s specific classes

In addition to Activity jump, we often use fragments in the development process. A common style is that the application home page Activity contains several fragments belonging to different components. Normally, we instantiate fragments directly by accessing the Fragment class, but in order to decouple modules from components, we can remove components without failure because the Fragment referenced does not exist. We don’t have direct access to the component’s Fragment class in the module.

We can still solve this problem through reflection by initializing the Fragment object and returning it to the Activity, adding the Fragment to a specific location in the Actiivty.

We can also use our componentBase module to implement this function. We can put the Fragment initialization in each component. When a module needs to use a component Fragment, The Fragment is initialized by the method in the Service provided by ComponentBase.

Here we use the second method to provide a UserFragment in the Login component to demonstrate.

First, create the UserFragment in the Login component, and then add the newUserFragment method to the IAccountService interface to return a Fragment. Implement this method in the empty implementation classes of the IAccountService in the Login component AccountService and componentBase, In the main module, you can use the ServiceFactory to obtain the IAccountService implementation object and invoke its newUserFragment to obtain the UserFragment instance. Here is the main code:

// IAccountService (componentBase) public interface IAccountService (componentBase) { /** * Create UserFragment * @param Activity * @param containerId * @param manager * @param bundle * @param Tag * @return*/ Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag); } AccountService implements IAccountService public class AccountService implements IAccountService public class AccountService implements IAccountService {// Other code... @Override public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) { FragmentTransaction transaction = manager.beginTransaction(); // Create a UserFragment instance and add it to the Activity. Fragment UserFragment = new UserFragment(); transaction.add(containerId, userFragment, tag); transaction.commit();returnuserFragment; }} public class FragmentActivity extends AppCompatActivity {@override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);setContentView(R.layout.activity_fragment); / / by component provides the Service implementation fragments of instantiation ServiceFactory. GetInstance () getAccountService () newUserFragment (this, R.id.layout_fragment, getSupportFragmentManager(), null,""); }}Copy the code

This enables Fragment instantiation, meets the requirement of decoupling, and ensures that business separation will not cause compilation failure and App crash.

6. Component integration debugging

The above problems are mainly problems that must be solved during the component development process. When the component development is complete, we may need to integrate a few components for debugging, rather than all components for debugging. At this point, we need to be able to compile with only some components integrated, and not fail because some components are not integrated.

In fact, we have already solved this problem in solving the above problems. Neither components nor modules operate directly with their classes, but instead with services in ComponentBase. The empty implementations of all Service interfaces in ComponentBase guarantee that even if a particular component is not initialized, There are no exceptions when other components call their corresponding methods. This interface – oriented programming allows us to decouple both components and modules from components.

The componentized architecture diagram then looks like this:

Goal of component decoupling and code isolation

Decoupling the target

The primary goal of code decoupling is complete isolation between components, and it’s important to keep in mind during development that not only can we not use classes directly from other components, but it’s better not to know the implementation details at all.

Code isolation

As you can see from the above solutions, we are trying to avoid direct references to classes between components and between modules and components. Even though we solved the problem of direct reference classes by providing services in ComponentBase, we implemented dependencies on the Login and Share components in the main project. The classes in the Login and Share components are still accessible in the main project.

In this case, even if the goal is to program to the interface, as long as you have direct access to the classes in the component, there is a possibility of using the code in the component by accessing the classes directly, either intentionally or unintentionally. If that happens, the decoupling above would be completely useless.

We want component dependencies to refer directly to classes in the component only during the packaging process, and we don’t have access to any classes in the component during development. Only by achieving this goal can we fundamentally eliminate the problem of direct reference to classes in components.

Gradle 3.0 provides a new dependency method called runtimeOnly. Dependencies are only available to modules and their consumers at runtime. The code for a dependency is completely isolated from its consumer at compile time.

So changing the dependency on the Login component and Share component in the main project to a runtimeOnly one solves the problem of direct references to classes in the component during development.

Build. gradle dependencies {// Other dependencies... runtimeOnly project(':login')
    runtimeOnly project(':share')}Copy the code

Once the code isolation problem is solved, another problem resurfaces. Component development requires not only code isolation, but also resource file isolation. RuntimeOnly, which addresses code isolation, does not do resource isolation. After relying on a component with runtimeOnly, the resource files in the component can still be used directly in the main project.

To solve this problem, we can add a resourcePrefix configuration to each component’s build.gradle to fix the resourcePrefix in the component. However, the resourcePrefix configuration can only limit the resources defined in the XML file in the res, not the image resources, so we need to manually limit the resourcePrefix when adding the image resources to the component. And put the resources that will be used in multiple components into the Base module. In this way, we can maximize the isolation of resources between components.

If a component is configured with resourcePrefix and the resource defined in its XML is not prefixed with the value of resourcePrefix, the resource defined in the corresponding XML will appear in red. The value of resourcePrefix is the prefix of the XML resource in the specified component. Take the Login component as an example:

// Login component build.gradle Android {resourcePrefix"login_"// Other configurations... }Copy the code

After adding the resourcePrefix configuration to the Login component, we will find that the resources defined by XML in the RES report red:

After we modify the prefix, the red will disappear and the display will return to normal:

At this point, the problem of code and resource isolation between components is resolved.

Eight, summary

By solving the six problems mentioned above, the major problems encountered in componentized development have all been solved. The most critical is the decoupling between modules and components. At the beginning of the design, several current mainstream componentization schemes were also referenced. Later, considering the difficulties of use, understanding, maintenance and expansion, the current componentization scheme was finally determined.

The Demo address: https://github.com/renxuelong/ComponentDemo

Welcome to follow my wechat official account “Yu Gang said” to receive first-hand technical dry goods