1. Why componentization

With the iteration of project requirements, the scale of the project is getting larger and larger, and various businesses are intricately interwoven together. The code has no constraints, the boundary is becoming more and more blurred, and the development and testing efficiency is getting lower and lower. In this case, we need to consider the use of componentized architecture to reconstruct the project, split the basic components and business components, and remove the coupling between business.

  • Single responsibility: Developers only care about the business they are responsible for
  • Code decoupling: Components are not directly dependent before, and the compiler is isolated
  • Strong reusability: basic components are decoupled to facilitate reuse of other projects
  • Compilation integration: Business components can be debugged independently to improve compilation speed
  • Platformization: Large teams are often split and platformized according to their business. Componentization helps to define their code boundaries and improve the efficiency of team coordination

Second, componentized architecture design

Architectural layering

  • Application layer: App shell project, Application, SplashActivity, etc
  • Business components: Divided by business modules, such as home page and discovery
  • Common services: Common basic services, such as BaseActivity, burying points, network library encapsulation, etc
  • Base components: Base libraries that are not relevant to the business, such as logging libraries, encryption and decryption, etc
  • Dependency library: a tripartite library of dependencies

Component split

Android Studio can split projects into multiple modules, one for each component. Once the component code is stable, you can upload maven libraries and rely on them using Gradle.

Component naming conventions

The hierarchy named
The application layer app
The business component Bussniss_home, bussniss_find
General business Common, service
Based on the component Lib_log, lib_ui, lib_utils

dependencies

  1. Following the dependency inversion principle, the upper layer depends on the lower layer, and the business layers do not depend on each other
  2. Common relies on common base components and third-party libraries. You are advised to use the API to rely on common, so that the build.gradle of business components directly depends on Common
  3. Business components rely on base components and dependent libraries in a implementation manner
  4. App dependencies on business components are runtimeOnly

Component communication

Business components are completely isolated from each other, with no direct dependencies, but there are scenarios where components need to communicate, such as page jumps and method calls. After comprehensive comparison, we finally chose ARouter as the basic scheme of communication. The overall scheme is interface + implementation. Each business component abstracts the interfaces that need to be exposed externally. The abstract interface class is put into a unified Service service component, and the implementation class is put into each business.

Page jump

ARouter.getInstance().build(RouterPathConstant.PATH_FIND).navigation();
Copy the code

The method call

Take user component as an example to illustrate the implementation of method invocation between components.

Service The IUserService of the service component, which defines the interface exposed by the user component.

public interface IUserService extends IProvider {
    public String getUserId(a);
    public String getNickname(a);
    public String getAvatar(a);
}
Copy the code

Bussiness_user Component interface implementation class UserServiceImpl.

@Route(path = ServicePathConstant.SERVICE_USER)
public class UserServiceImpl implements IUserService {
    @Override
    public void init(Context context) {}public String getUserId(a) {
        return UserCenter.getInstance().getUserId();
    }

    public String getNickname(a) {
        return UserCenter.getInstance().getNickname();
    }

    public String getAvatar(a) {
        returnUserCenter.getInstance().getAvatar(); }}Copy the code

Now that the interface definition and implementation are OK, how do I expose the interface class so that the caller can get the IUserService object? ARouter is also used to implement the service factory class ServiceFactory to manage the abstract interface in a unified manner. ServiceFactory is located in the Service component and can be directly called by all business components.

public class ServiceFactory {

    public static IUserService getUserService(a) {
        return (IUserService) getService(ServicePathConstant.SERVICE_USER);
    }

    private static Object getService(String path) {
        returnARouter.getInstance().build(path).navigation(); }}Copy the code

If you don’t use ARouter, you can also define the set method in ServiceFactory. The UserServiceImpl is instantiated and assigned to ServiceFactory during Application initialization.

At this point, we can invoke the user component’s methods from within any business component.

String userId = ServiceFactory.getUserService().getUserId();
Copy the code

Call the app method

Above we talked about dependency inversion. Business components cannot depend on the APP layer. During the componentized step-up refactoring, there may still be some business code in the App Module. If the business component needs to call this part of the code, you can define the IUserDelegator interface and set the implementation class to IUserService during app initialization.

(IXXService is the exposed interface of the business component, IXXDelegator is the interface that the business component needs the caller to implement. Don’t confuse the two.)

Service component adds IUserDelegator interface class.

public interface IUserDelegator {
    void go2Main(a);
    void go2Spalsh(a);
}
Copy the code

Service component IUserService adds setDelegator and getDelegator methods.

public interface IUserService extends IProvider {
    void setDelegator(IUserDelegator delegator);
    IUserDelegator getDelegator(a);
}
Copy the code

App Module implements IUserDelegator and calls setDelegator of IUserService.

public class WalletApplication extends Application {
    @Override
    public void onCreate(a) {
        super.onCreate();
        
        ServiceFactory.getUserService().setDelegator(UserDelegatorImpl());
    }
Copy the code

At this point, the business_user component is ready to call the app’s methods.

IUserDelegator delegator = ServiceFactory.getUserService().getDelegator();
delegator.go2Main();
Copy the code

Iv. Resource file management

Of course, resource files should also be split into business modules. To avoid conflicts, it is strongly recommended to use a uniform prefix for resource files. For example, r.rawable. User_icon_back, r.layout. user_activity_edit, r.string.user_dialog_title. The duplicate resource file name in module does not affect compilation, but only 1 resource file will be packed in. Why does my layout file change but it does not take effect?

Build. Gradle can add a resource file prefix check and Android Studio will warn you if you don’t name it properly.

defaultConfig {
    minSdkVersion rootProject.ext.android.minSdkVersion
    targetSdkVersion rootProject.ext.android.targetSdkVersion
    resourcePrefix "user_"
}
Copy the code

5. Component integration and debugging

Some projects will require multiple apps to be packaged, such as sock puppet packs and multi-country versions. This requires multiple projects and multiple App Modules, in which the required business components are selected and packaged to generate different APKs.

As mentioned earlier, app dependencies on business components use the runtimeOnly method, which has the advantage of decoupling app and business components. For example, the app should not access the UserFragment directly. Instead, it should provide access methods in the IUserService. The instantiation of the Fragment is stored in the Bussiness component.

public interface IUserService {
    public Fragment newUserFragment();
}
Copy the code

Component independent compilation debugging

At present, we compile the project for 2-3 minutes, which is tolerable, so we did not compile the component independently for the time being. If the team size is large and the project compilation is slow, it can also be done.

Six, think

  • Coupling and redundancy can sometimes conflict

    For example, multiple businesses need to call an API, so does the RESPONSE model of the API need to be extracted into the common component? There’s no coupling when you break it down into business components, but it feels weird looking at a bunch of redundant code. Decoupling and redundancy, it’s up to us.

  • How to manage constants

    I’ve seen some engineering code that likes to define a global Constants class that modules reference, or even sink Constants into the Common module. This is not a good practice; constants should be spread across business components and not shared between modules.

  • High cohesion and low coupling

    It’s important to keep these six words in mind when refactoring code. Componentalization is not about breaking up modules, it’s about writing code with high cohesion and low coupling.

  • Code cleanliness

    Finally, I hope we keep code cleanliness, do a clean program ape.