Why componentization

Small projects do not need to be componentized. When a project has dozens of people working on it, takes 10 minutes to compile, fixes a bug can affect other businesses, and small changes require regression testing, then we need to compontize

Componentization and modularization

In the process of technological architecture evolution, modularization must appear first and then componentization, because componentization solves the modularization problem.

Modular architecture

After creating a Project, you can create multiple modules, which are called modules. A simple example, when writing the code we might open the homepage, news, my module, containing the contents of each TAB is A module, which reduces the amount of code the module, but is certainly A page jump between each module, data transmission, etc., such as A module need B module data, So we will use implementation Project (‘:B’) in gradle file of module A to rely on module B, but module B needs to jump to A page of module A, so module B depends on module A again. Such a development model is still not decoupled, fixing a bug will still change many modules, and will not solve the problems of large projects.

Componentized architecture

Here we first mention a few concepts. The components developed by our daily business requirements are called business components. If the business requirements can be widely reused, they are called business base components, such as picture loading, network request and other framework components. The app component that builds all the components is called the shell component/project.

To start with a few concepts, the components of our daily business requirements development are calledThe business componentIf the business requirement is universally reusable, it is calledBusiness base Components, such as image loading, network requests and other framework components we callBased on the component. Name of the APP component that builds all the componentsFor shell components/engineering. Here’s an architecture diagram:

Solid lines represent direct dependencies and dotted lines represent indirect dependencies. For example, shell projects must rely on business base components, business components, and module_common libraries. Business components rely on business underlying components, but not directly, but indirectly through “sinking interfaces.” Dependencies between business components are also indirect dependencies. Finally, common component depends on all required basic components, common is also a basic component, it only unified the version of the basic component, but also provides some abstract base classes for applications, such as BaseActivity, BaseFragment, basic component initialization, etc..

The advantages of componentization

** Faster compilation: ** Each business component can run debugging separately, which is several times faster. For example, the independent compilation time of the video component is 3s, because at this time, AS will only run tasks of the video component and components that the video component depends on. If the integration compilation time is 10s, tasks of all components referenced by the APP will be executed. You can see that efficiency has increased by three times.

** Improve collaboration efficiency: ** Each component is maintained by a dedicated person, and there is no need to care about how other components are implemented, only the data needed by the other side is exposed. Testing also does not require a full regression, just focusing on testing the modified components.

** Functional reuse: ** code is reused everywhere, no need to copy code anymore. In particular, basic components and business components can be integrated and used with one click by basic users based on documentation.

As mentioned earlier, non-large projects are not typically componentized, but as mentioned above, this advantage is not limited to large projects. We can write requirements or libraries with componentization in mind as a single base component or business base component. When the second project comes along and needs this component, we save time to unpack the component (since writing requirements will likely create a lot of coupling and take time to unpack later), such as login components, share components, and so on, which can be written as components from the beginning.

Componentize the problem to be solved

How can business components be debugged separately?

Without dependencies between service components, how to achieve page hopping?

How do business components communicate without dependencies?

How to deliver the Application life cycle of shell engineering?

Independent debugging

Single project scheme

The so-called single project solution is to put all the components under a project, let’s take a look at the overall table of contents:

Ps: module_ indicates the basic component, fun_ prefix indicates the basic service component, biz_ prefix indicates the service component, and export_ prefix indicates the exposed interface of the service component.

Advantages and disadvantages of single project:

  • Pro: Once a module is modified, it simply compiles and other modules that depend on it immediately sense the change.
  • Con: Not a complete separation of responsibilities, with developers of each module having permission to modify other modules.

Start by declaring a variable in the gradle.properties file:

// gradle.properties
isModule = true
Copy the code

IsModule true means that the component can be run as apK, false means that the component can only be run as library. We can change this value as needed and synchronize Gradle.

Then use this variable in the build.gradle file of a module to make three decisions:

If (isModule.toboolean ()) {apply plugin: 'com.android. Application '}else {apply plugin: 'com.android. Library '} android {defaultConfig {// Application if(isModule.toboolean ()) {applicationId "Com.xxx.xxx"}} sourceSets {main {AndroidManifest file for applications and libraries if(isModule.toboolean ()) {manifest.srcfile 'src/main/debug/AndroidManifest.xml' }else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } }Copy the code

Since the Library does not require the Application and Activity start page, we need to distinguish this file. The path specified by the manifest Application is not specified. In the androidmanifest.xml application we need to set the launch page:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sun.biz_home">
    <application
        android:allowBackup="true"
        android:label="@string/home_app_name"
        android:supportsRtl="true"
        android:theme="@style/home_AppTheme">
        <activity android:name=".debug.HomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
Copy the code

Library’s Androidmanifest.xml does not require this:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sun.biz_home">
</manifest>
Copy the code

Gradle relies on modules in two main ways:

  • Implementation: A implementation B, B implementation C, but A can’t access C.
  • API: A API B, B API C, things that A can access C.

Generally speaking, we just need to use implementation. The API will cause the project to compile longer and introduce features that the module does not need. The coupling between the code becomes serious. However, module_common is a common library that unites the base component version, and all components should need to rely on it and have the capabilities of the base component, so basically every business component and business base component should rely on the common library:

dependencies {
	implementation project(':module_common')
}
Copy the code

The common component relies on the underlying component using the API, because the capabilities of the underlying component are passed to the upper business component:

dependencies {
	api project(':module_base')
	api project(':module_util')
}
Copy the code

Multiple engineering scheme

Multi-project means that each component is a project. For example, after creating a project, APP is used as a shell component. It relies on biz_HOME to run, so it does not need isModule to control independent debugging.

The pros and cons of multiple projects are the opposite of single projects:

  • Advantages: complete separation of responsibilities, more convenient reuse of other projects, direct line dependency introduction.
  • Disadvantages: Changes need to be uploaded to the Maven repository, and other projects can only perceive the changes after compiling again, which increases the upload and compilation time.

Multi-project component dependencies require maven repositories. Upload the AAR for each component to the Maven repository on the corporate Intranet and derely it like this:

Implementation 'com. XXX. XXX: module_common: 1.0.0'Copy the code

As we learn about componentization, we can build a Maven repository and upload an AAR to our own Maven repository. Here is an introduction to building a Maven repository and uploading an AAR. Note that we use the snapshot version in the development phase and do not change the version number to upload, but increase the version number and upload when releasing the official version.

Gradle: config. Gradle:

Ext {dependencies = [" glide ":" com. Making. Bumptech. Glide: glide: 4.12.0 ", "glide - the compiler" : "Com. Making. Bumptech. Glide: the compiler: 4.12.0", "okhttp3" : "com. Squareup. Okhttp3: okhttp: 4.9.0", "retrofit" : "Com. Squareup. Retrofit2: retrofit: 2.9.0", "retrofit converter -- gson" : "Com. Squareup. Retrofit2: converter - gson: 2.9.0", "retrofit - adapter - rxjava2" : "Com. Squareup. Retrofit2: adapter - rxjava2:2.9.0", "rxjava2" : "IO. Reactivex. Rxjava2: rxjava: 2.2.21", "arouter" : Com.alibaba: arouter-API :1.5.1", "arouter-compiler": "com.alibaba:arouter-api:1.5.1", // our lib "module_util": "Com. Sun. The module: module_util: 1.0.0", "module_common" : "com. Sun. The module: module_common: 1.0.0", "module_base" : "Com. Sun. The module: module_base: 1.0.0", "fun_splash" : "com. Sun. Fun: fun_splash: 1.0.0", "fun_share" : "Com. Sun. Fun: fun_share: 1.0.0", "export_biz_home" : "com. Sun. Export: export_biz_home: 1.0.0", "export_biz_me" : "Com. Sun. Export: export_biz_me: 1.0.0", "export_biz_msg" : "com. Sun. Export: export_biz_msg: 1.0.0", "biz_home" : "Com. Sun. Biz: biz_home: 1.0.0", "biz_me" : "com. Sun. Biz: biz_me: 1.0.0", "biz_msg" : "com. Sun. Biz: biz_msg: 1.0.0"]}Copy the code

Build. Gradle > build. Gradle > build.

apply from: 'config.gradle'
Copy the code

Finally, introduce dependencies in the respective modules, such as module_common.

dependencies {
	api rootProject.ext.dependencies["arouter"]
  kapt rootProject.ext.dependencies["arouter-compiler"]
  api rootProject.ext.dependencies["glide"]
  api rootProject.ext.dependencies["okhttp3"]
  api rootProject.ext.dependencies["retrofit"]
  api rootProject.ext.dependencies["retrofit-converter-gson"]
  api rootProject.ext.dependencies["retrofit-adapter-rxjava2"]
  api rootProject.ext.dependencies["rxjava2"]
  api rootProject.ext.dependencies["module_util"]
  api rootProject.ext.dependencies["module_base"]
}

Copy the code

Personally, I think multiple projects are suitable for “very large” projects. Each business component may require a group development, such as Taobao app. However, this is only for business components. Business base components and base components are not modified very often and are best used as a single project uploaded to the Maven repository. The examples in this article are written together for convenience. It is best to separate the fun_ and module_ components into a single project and write the business components into a single project.

Page jump

After the isolation between components, the most obvious problems exposed are page hops and data communication problems. Generally speaking, a page jump is a startActivity jump, which is not applicable in componentialized projects. Implicit jumps can be used, but it is a bit cumbersome to write an intent-filter for each Activity, so it is best to use a routing framework.

In fact, there are mature routing frameworks in the market that are specifically for componentization, such as WMRouter of Meituan and ARouter of Ali. In this case, ARouter framework is used to look at the basic operation of ARouter page jump.

Module_common to ARouter, build.gradle should add:

android {
	defaultConfig {
		javaCompileOptions {
       annotationProcessorOptions {
          arguments = [AROUTER_MODULE_NAME: project.getName()]
     	 }
    }
	}
	compileOptions {
      sourceCompatibility JavaVersion.VERSION_1_8
      targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
	api rootProject.ext.dependencies["arouter"]
  kapt rootProject.ext.dependencies["arouter-compiler"]
}
Copy the code

Kapt annotations rely on there is no way to transfer, so we inevitably have to need to declare these in each module configuration, in addition to API rootProject. Ext dependencies [” arouter “] this line. Then you need to register ARouter globally, which I registered uniformly in module_common.

class AppCommon: BaseApp{
    override fun onCreate(application: Application) {
        MLog.d(TAG, "BaseApp AppCommon init")
        initARouter(application)
    }

    private fun initARouter(application: Application) {
        if(BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(application)
    }
}
Copy the code

Next we declare a routing table within the module_common module to be used as a unified management path.

// RouterPath.kt
class RouterPath {
    companion object {
        const val APP_MAIN = "/app/MainActivity"
        const val HOME_FRAGMENT = "/home/HomeFragment"
        const val MSG_FRAGMENT = "/msg/MsgFragment"
        const val ME_FRAGMENT = "/me/MeFragment"
        const val MSG_PROVIDER = "/msg/MsgProviderImpl"
    }
}

Copy the code

Then annotate the MainActivity class file:

@Route(path = RouterPath.APP_MAIN)
class MainActivity : AppCompatActivity() {
}

Copy the code

Any module simply calls arouter.getInstance ().build(RouterPath.app_main).navigation() to jump. It is also convenient if we want to add data passing:

ARouter.getInstance().build(RouterPath.APP_MAIN)
            .withString("key", "value")
            .withObject("key1", obj)
            .navigation()

Copy the code

Then use dependency injection to receive data in MainActivity:

class MainActivity : AppCompatActivity() {
    @Autowired
    String key = ""
}
Copy the code

Arouter scheme

Declare IMsgProvider under the export_biz_msg component. This interface must implement the IProvider interface:

interface IMsgProvider: IProvider {
    fun onCountFromHome(count: Int = 1)
}

Copy the code

Then implement this interface in the biz_msg component:

@Route(path = RouterPath.MSG_PROVIDER) class MsgProviderImpl: IMsgProvider { override fun onCountFromHome(count: Int) {/ / here is the data distribution, a listen to count objects will receive MsgCount. Instance. AddCount (count)} override fun init (context: context? {// called when the object is initialized}}Copy the code

Send count in biz_HOME home component:

val provider = ARouter.getInstance().build(RouterPath.MSG_PROVIDER).navigation() as IMsgProvider
provider.onCountFromHome(count)
Copy the code

As you can see, it’s basically the same as page hopping, including the way to get the Fragment instance. ARouter implements all communication methods in one API, making it very easy for users to get started.

Application lifecycle distribution

When the app shell project starts Application initialization, it notifies other components to initialize some functionality. Here’s a simple way to do it.

First we declare an interface BaseApp inside the module_common library:

interface BaseApp {
    fun onCreate(application: Application)
}

Copy the code

Each component then creates an App class that implements this interface, such as the biz_HOME component:

class HomeApp: BaseApp { override fun onCreate(application: Mlog. d(TAG, "BaseApp HomeApp init")}}Copy the code

The final step is to distribute the application lifecycle from the app shell project, using reflection:

val moduleInitArr = arrayOf(
    "com.sun.module_common.AppCommon",
    "com.sun.biz_home.HomeApp",
    "com.sun.biz_msg.MsgApp",
    "com.sun.biz_me.MeApp"
)
class App: Application() {
    override fun onCreate() {
        super.onCreate()
        initModuleApp(this)
    }
    private fun initModuleApp(application: Application) {
        try {
            for(appName in moduleInitArr) {
                val clazz = Class.forName(appName)
                val module = clazz.getConstructor().newInstance() as BaseApp
                module.onCreate(application)
            }
        }catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

Copy the code

We just need to know the fully qualified name of each Class that implements the BaseApp interface and write it into the moduleInitArr array, and then get the Class object by reflection to get the constructor to create the entity object, Finally, the BaseApp onCreate method is called to pass in the application, and every application lifecycle method can be passed in this way.