1, an overview of the

I work in the development of intelligent furniture industry, but also from the company entrepreneurial teams to work until now, for the company’s project from version 1.0 began to take over until now, the project is not very big but the sparrow is small all-sided, in the expansion of the projects and teams, also don’t increase, the problems exposed by the componentization is imperative, in this paper, according to the development of the whole project, Summarize the practice process of componentization;

2. Componentization basis

Before proceeding to componentization, distinguish between two concepts: modularization and componentization

  • Componentalization: a single functional component can be developed independently and separated from business programs to realize the reuse of components, such as Bluetooth components and music components
  • Modularity: Modularity is mainly for business. Separate business functions are developed, and codes are decoued between each functional module. Modules can be added or removed freely during compilation, such as community module and e-commerce module

As we know from the above introduction, componentization is aimed at finer and single businesses with larger granularity of function modules and the overall business of a certain aspect. Of course, many independent components may be used in the business, and 3.0 is entered according to the architecture of componentization requirements and projects

  • Basic layer: mainly encapsulates common tool classes and some encapsulated basic libraries, such as data storage, network request, etc
  • Component layer: Decouples independent functional components from a single supply
  • Business module layer: separate independent and similar business modules and introduce corresponding functional components according to their requirements
  • APP layer: APP layer is the top layer of the project, which combines all functional modules in the APP framework to realize the compilation of a real APP

3. Componentization

According to the above 3.0 architecture, the project contains multiple functional components and business modules. During development, it is necessary to ensure that components cannot be coupled, and business blocks depend on components, but business modules cannot refer to each other, otherwise it violates the principle of componentization.

  • The ultimate goal of componentization
  1. Realize the code decoupling and code isolation between components and modules, reduce the maintenance cost of the project
  2. Implement component reuse
  3. Realize separate debugging and overall compilation of functional components and business modules to reduce the development and compilation time of the project
  • Componentize problem to be solved
  1. Implementation components can be compiled individually or as a whole, shortening the compilation time of the program
  2. How can applications be dynamically configured in components and Modules
  3. Data transfer between components
  4. Interface jumps between components and modules
  5. Decoupling between the main project and business modules, enabling the addition and removal of modules
3.1. Separate debugging of components
  • In Android development, Gradle offers three forms of build:
  1. App plug-in, ID: com.android.application
  2. Library plugin, id: com.android.libray
  3. Test plug-in, id: com.android.test

In our actual development, app is built in the form of Application, which is eventually compiled into APK file, and other modules are compiled in the form of Library. Finally, API calls are provided in the form of ARR. In other words, independent compilation can be realized as long as the compilation form of components is modified. So create a gradle.properties file under the component to control the build form

isRunAlone = false
Copy the code

Modify the build form in build.gradle based on the isRunAlone variable

if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
Copy the code
  • Configuration applicationId
    if (isRunAlone.toBoolean()) {
            applicationId "com.alex.kotlin.content"
        }
Copy the code
  • Configure the AndroidManifest file

In modular separate compilation and overall compile time, the list of registered need different content, such as separate compilation need extra start page, and separately compile time also have to configure the different Application, at this point in the main file and create the manifest/AndroidMenifest. The XML file, Set the content according to the needs of the separate compilation.

  1. The overall compilation
  2. Separate compilation
  3. Configure different file paths in build.gradle
sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'}}}Copy the code

If you need to compile separately, change isRunAlone to true.

3.2. Components dynamically initialize applications

As can be seen from the two registry files configured above, the component uses the global Application when compiling the App as a whole, and the AutoApplication is used when compiling the App separately. As we all know, there is only one Application class in a program. The code that needs to be initialized in the component is configured in its own AutoApplication. How do you initialize it at compile time? The main project can’t add or subtract modules freely, and the AutoApplication can’t be seen when code is isolated. Here, a configuration + reflection method is adopted to make the Application of each component comfortable. The specific implementation is as follows:

  • The BaseApp abstract class is declared in the Base component, and BaseApp inherits the Application class
Abstract Class BaseApp: Application(){/** * initModuleApp(Application: Application) }Copy the code
  • Implement this BaseApp class in the component and configure the code initialized at global compilation time for initModuleApp ()
class AutoApplication : Override fun onCreate() {// Initialize super.oncreate () multidex.install (this) apputils.setContext (this) initModuleApp(this) ServiceFactory.getServiceFactory().loginToService = AutoLoginService() } override fun initModuleApp(application: Application) {/ / compile overall ServiceFactory getServiceFactory () serviceIntelligence = AutoIntelligenceService ()}}Copy the code
  • Create the AppConfig class in the Base component and configure the BaseApp subclass to load at initialization
object AppConfig {
    private const val BASE_APPLICATION = "com.pomelos.base.BaseApplication"
    private const val CONTENT_APPLICATION = "com.alex.kotlin.content.ContentApplication"
    private const val AUTO_APPLICATION = "com.alex.kotlin.intelligence.AutoApplication"

    val APPLICATION_ARRAY = arrayListOf(BASE_APPLICATION, CONTENT_APPLICATION, AUTO_APPLICATION)
}
Copy the code
  • Reflection calls all applications in the main Application
public class GlobalApplication extends BaseApp {
	@SuppressLint("StaticFieldLeak")
	private static GlobalApplication instance;
	public GlobalApplication(a) {}
	@SuppressWarnings("AlibabaAvoidManuallyCreateThread")
	@Override
	public void onCreate(a) {
		super.onCreate();
		MultiDex.install(this);
		AppUtils.setContext(this);
		if (BuildConfig.DEBUG) {
			/ / open the Debug
			ARouter.openDebug();
			// Enable log printing
			ARouter.openLog();
		}
		// Initialize ARouter
		ARouter.init(this);
		ServiceFactory.Companion.getServiceFactory().setLoginToService(new AppLoginService());
		// Initializes the component's Application
		initModuleApp(this);
	}
	@Override
	public void initModuleApp(@NotNull Application application) {
		for (String applicationName : AppConfig.INSTANCE.getAPPLICATION_ARRAY()) { // Iterate through all configured applications
			try {
				Class clazz = Class.forName(applicationName); // reflection executes
				BaseApp baseApp = (BaseApp) clazz.newInstance(); // Create an instance
				baseApp.initModuleApp(application); // Perform initialization
			} catch(ClassNotFoundException e) { e.printStackTrace(); }}}}Copy the code

This is done by configuring all Application paths in AppConfig, creating each instance when the main Application executes, and calling the corresponding initModuleApp (). Did you notice that in AutoApplication the content is also initialized in onCreate (), which is called at compile time alone;

3.3 Data transfer between components

In the project because sometimes need APK packaging different needs, so I login to login separately to isolate into components the same behavior, then after the spy module relies on the login login function can be realized, but each individual business separate compilation will produce multiple APK, need to get all these APK login status and jump the corresponding interface, So how do you do that without decoupling your program? The answer is timely implemented using the registration interface;

  1. Declare the LoginToService interface in the Base component
interface LoginToService {
    /** * where to go after login */
    fun goToSuccess(a)
}
Copy the code
  1. Create ServiceFactory in base and provide calls to the singleton
class ServiceFactory private constructor() {
    companion object {
        fun getServiceFactory(a): ServiceFactory {
            return Inner.serviceFactory
        }
    }
    private object Inner {
        val serviceFactory = ServiceFactory()
    }
}
Copy the code
  1. Declare the LoginToService object in ServiceFactory, along with an empty implementation of LoginToService
var loginToService: LoginToService? = null
        get() {
            if (field == null) {
                field = EmptyLoginService()
            }
            return field
        }
Copy the code
  1. In the corresponding business module to implement LoginToService, rewrite the method to set the interface to jump to
class AppLoginService : LoginToService {//App module
    override fun goToSuccess(a) {
        val intent = Intent(AppUtils.getContext(), MainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        AppUtils.getContext().startActivity(intent)
    }
}

class AutoLoginService : LoginToService {// Intelligent module
    override fun goToSuccess(a) {
        val intent = Intent(AppUtils.getContext(), AutoMainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        AppUtils.getContext().startActivity(intent)
    }
}
Copy the code
  1. Register the respective instances with ServiceFactory in the initialization Application
ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
Copy the code
  1. Once you have logged in to the Login component, you can jump by calling the method registered object in ServiceFactory
override fun loadSuccess(loginBean: LoginEntity){ ServiceFactory.getServiceFactory().loginToService? .goToSuccess() }Copy the code

Each component registers with the ServiceFactory in the Base component to provide functions to be executed externally. Because the ServiceFactory singleton is invoked, the method can be executed after obtaining the registered instance from the ServiceFactory in other components. To prevent errors when subtracting components or modules, An empty implementation of the service is also provided in Base;

3.4 Interface jump between components

Ali’s ARoute framework is recommended for page-jumping, as detailed in another article: Android Framework source Code Analysis – Using Arouter as an example to talk about the best way to learn open source frameworks

3.5 decoupling between the main project and the business module

In general projects, the first interface of the main app is composed of different business modules. The most common one is the combination of Fragment and ViewPager of different components. However, the main app needs to obtain the Fragment instance of the component, which cannot be directly used according to the idea of componentization. Otherwise, the main APP and components and modules will be coupled together again. Here, interface mode is also adopted for processing, and the process and data interaction are roughly the same.

  • Declare the interface in the base component and implement the interface in the corresponding module
interface ContentService {
    /** * returns the instantiated Fragment */
    fun newInstanceFragment(a): BaseCompatFragment?
}
// Content module implementation
class ContentServiceImpl : ContentService {
    override fun newInstanceFragment(a): BaseCompatFragment? {
        return ContentBaseFragment.newInstance() // Provide a Fragment object}}Copy the code
  • Register the service during Application initialization
   ServiceFactory.getServiceFactory().serviceContent = ContentServiceImpl()
Copy the code
  • Get it from ServiceFactory in the main App
mFragments[SECOND] = ServiceFactory.getServiceFactory().serviceContent? .newInstanceFragment()Copy the code
3.6. Other questions
  • Code isolation

Although going through componentation decouples code, how can you make the methods in a component or module invisible if the methods in the dependent component or module are always visible during development, and if the code in the module is used during development, then the programs are coupled together? The answer lies in the runtimeOnly dependency, which can isolate code during development and make it visible at compile time

    runtimeOnly project(':content')
    runtimeOnly project(':intelligence')
Copy the code
  • Resource isolation

RuntimeOnly dependencies implement code isolation, but have no effect on resources, and may still be used to reference resources directly. To prevent this, the resources of each component are prefixed with a unique prefix

  resourcePrefix "auto_"
Copy the code

All resources in this Module must start with auto_ otherwise they will be warned;

  • ContentProvider

Because ContentProvider is used in the project, the overall compilation and installation of the mobile phone can run normally, at this time to separate compilation is always prompted installation failure. The final reason is that the ContentProvider and permissions in the two apks are the same, so how to ensure that the separate compilation and the whole compilation have different permissions, so that the installation is successful? We first configure the Provider in the Menifest file above

  • Separate compilation
  <provider
            android:name=".database.MyContentProvider"
            android:authorities="com.alex.kotlin.intelligence.database.MyContentProvider"
            android:exported="false" />
Copy the code
  • The overall compilation
<provider
            android:name=".database.MyContentProvider"
            android:authorities="com.findtech.threePomelos.database.MyContentProvider"
            android:exported="false" />
Copy the code

In this way, two providers with different permissions can be installed successfully. When using the ContentProvider, you need to execute the ContentProvider according to the permissions. Then how to concatenate the corresponding execution permissions according to the different compilation types in the code? Gradle allows you to configure BuildConfig directly in BuildConfig and get it at use

 if (isRunAlone.toBoolean()) {
            buildConfigField 'String'.'AUTHORITY'.'"com.alex.kotlin.intelligence.database.MyContentProvider"'
        }else {
            buildConfigField 'String'.'AUTHORITY'.'"com.findtech.threePomelos.database.MyContentProvider"'
        }
        
   const val AUTHORITY = BuildConfig.AUTHORITY / / use
Copy the code

4, summarize

After solving all the problems above, basic can implement componentization of the project, but the division of specific size and details, need their own business and experience to deal with, may need to separate module directly, some may also have the function of the small must be placed on the base components of sharing, and each person has the different way for each project, Just understand the ideas and ways of componentization to achieve the final requirements;