Introduction: Componentization, plug-in development, will be an indispensable part of the future Android. Today share from Zhang Huayang Zhang Huayang blog address: http://blog.csdn.net/guiying712, click here to read the original, can see corresponding to the original, plan the Demo: https://github.com/guiying712/AndroidModulePattern


1. Android componentization project

Before Android componentization project, our project is like the image below, a single project, according to the different business points several folders, under the third-party libraries need to rely on began to develop, such code coupling is serious, inescapably, delete code will be somewhere around an error, if you don’t get rid of an error, can’t compile packaging, And such code is only suitable for personal development, especially when the team development merge code that is really a trouble, I believe we will deeply experience, if the project is very large, modify a simple page will have to recompile, Android compilation speed we have seen, each packaging is very time-consuming, And there’s no way to unit test this code.

Therefore, componentization of Android project is imminent. The direction of componentization is to divide a project into several module projects, and the main project of App provides a unified entrance. Each business independent module shares the Common dependency library of the project.

2. Implementation steps of Android componentization project

1) Step 1: Configure methods that automatically switch components between Application and Library properties

As we all know, modules in Android Studio have two main properties:

  • The application property, the Android application that can run independently, that is, our APP;

    Apply the plugin: ‘com. Android. Application’

  • Library attribute, can not run independently, generally is the Android application rely on library file;

    Apply the plugin: ‘com. Android. Library’

When we develop a single component, the component should be in Application mode, and when we merge the single component into the main project, we need to change the single group from Application mode to Library mode. Maybe you can change your build.gradle file every time you switch, but if your project has dozens of components, do you want to change them one by one? Therefore, we must have a way to dynamically switch component modes, so that the global component takes effect once the change is made. This problem needs to be solved by configuring Gradle.

In the root directory of the Android Studio project, there is a gradle.properties file. This file is used to configure gradle Settings, such as JVM parameters. Want to know more of this file please check www.gradle.org/docs/curren… Gradle. properties fields can be read directly from build.gradle without any extra code.

Now we add a line of code to gradle.properties that defines a property isModule (component development mode, true for yes, false for no) :

After changing the value of "isModule", you need to click the "Sync Project" button isModule=trueCopy the code

We then read this line of code in the component’s build.gradle file:

if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}Copy the code

Since gradle.properties is a String and we need a Boolean, we need to convert String to Boolean. If it is in Component development mode, we will apply this component to application mode. If not, apply the component to library mode, which is a library. This solves our first problem. First we define an isModule property in gradle.properties and then read this property from each component’s build.gradle. Whenever we need to switch from component development mode to APP development mode, You only need to change the value of “isModule”. Of course, the comment also says that after changing the value of “isModule”, you need to click the “Sync Project” button on AndroidStudio to synchronize the whole Project.

2) Step 2: Solve the problem of merging component AndroidManifest and main project AndroidManifest

Each component is developed separately by a different member. In this case, the component is a separate APP. Then the component has its own “Androidmanifest.xml”, whereas Android applications have only one “Androidmanifest.xml”. When we merge a component into the main project as a Library, the component’s Androidmanifest.xml and the main project’s Androidmanifest.xml conflict because they both have their own implementation application classes and properties. It also has its own MAIN Activity, which can cause conflicts if you directly merge tables together.

The idea is to maintain two tables for each component, one for use when the component is developed separately and one for incorporation into the main project registry, adding both tables whenever an Android component is added.

We saw in the previous section how you can automatically switch between a component’s Application and Library properties. With this method, it is easy to maintain two tables. First create two folders in the component’s main folder (level with the Java folder), as shown below:

Then add the following code to *build.gradle for each component:

sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { Manifest. SrcFile 'SRC/main/release/AndroidManifest. XML' / / release mode to rule out all the Java files in the debug folder Java {exclude 'debug / * *}}} }Copy the code

When in component development mode, registry files in the debug folder are used for components, otherwise registry files in the Release folder are used. So what’s the difference between these two tables?

The following represents the debug folder:

<application android:name="debug.CarApplication" android:icon="@mipmap/ic_car_launcher" android:label="@string/car_name"  android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".query.QueryActivity" android:configChanges="orientation|screenSize|keyboard" android:screenOrientation="portrait" android:windowSoftInputMode="adjustPan|stateHidden"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".scan.ScanActivity" android:screenOrientation="portrait" /> </application>Copy the code

The following table is in the release folder:

 <application android:theme="@style/AppTheme">
    <activity
        android:name=".query.QueryActivity"
        android:configChanges="orientation|screenSize|keyboard"
        android:screenOrientation="portrait"
        android:theme="@style/AppTheme"
        android:windowSoftInputMode="adjustPan|stateHidden" />
    <activity
        android:name=".scan.ScanActivity"
        android:screenOrientation="portrait" />
 </application>Copy the code
  1. The specific Application class is specified in the registry label in the Debug folder, but not in the Release folder,

  2. The Debug folder adds some application properties to the registry TAB, while the Release folder adds nothing.

  3. The registry in the Debug folder specifies QueryActivity as the MAIN Activity, which is the Activity to start, but not in the Release folder;

3) Step 3: solve the Application conflict between the component and the main project as well as the initialization (sharing) data problem of the component development separately

When the Android program starts, the Android system will create an Application class object for each program, and only one. The life cycle of the Application object is the longest in the whole program, and its life cycle is equal to the life cycle of the program. By default the Application system automatically generates the Application object, but if we customize the Application, we need to tell the system that we instantiate our custom, not the default. However, in componentized development, each component may have an object of its own Application class. If we need to obtain the global Context when developing in our own component, we will generally directly obtain the Application object, but there will be problems when all components need to be packaged and merged together. Because there is only one Application at the end of the Application, the Application defined by our component cannot be used. We can’t change the global Application every time we package it.

Solution: First, create a Library called Common. This Common Library mainly contains Common base classes, tool classes and custom views used in the whole project, such as BaseActivity, BaseFragment, BaseApplication, etc. And each of our components depends on the Common library. Now we will mainly talk about how to define the Common library BaseApplication, the following is part of the code:

public class BaseApplication extends Application { private static BaseApplication sInstance; public static Context context; public static BaseApplication getIns() { return sInstance; } @Override public void onCreate() { super.onCreate(); sInstance = this; context = this.getApplicationContext(); If (isAppDebug(context)) {// Only debug mode prints log logger.init ("Demo").loglevel (loglevel.full); } else { Logger.init("Demo").logLevel(LogLevel.NONE); }}}Copy the code

Because each component relies on the Common library, each component can get baseApplication. context, but Android applications default to the system’s own Application class, To use our own, we need to inherit Application and declare it in androidmanifest.xml, so we first create a component Application in our own component and inherit it from BaseApplication, Then declare in the debug file androidmanifest.xml:

public class CarApplication extends BaseApplication { @Override public void onCreate() { super.onCreate(); login(); }}Copy the code

This allows us to use the global Context in the component: Context, but there is a problem. If we define CarApplication in our component, then the main project will have its own Application after the component is merged into the main project, which will conflict again. In fact, the code for this problem is already written in section 2. We only use CarApplication for component development, so we should have excluded this code when merging into the main project.

Create a debug folder in the Java folder, place the component’s own application in this folder, and add this line to build.gradle:

This eliminates all Java files in the Debug folder when merged into the main project. And you can do some initialization in the component’s Application, such as logging in, and then save the data for the component to use.

4) Step 4: Solve the problem of library repeated dependencies and Sdk and dependent third-party library version number control

Double dependencies are A common problem in development, such as when you compile an A, then compile A B in the library, and then compile the same B in your project, and then rely on it twice. By default, if it is an AAR dependency, Gradle will automatically find the new version of the library for us and discard duplicate dependencies from the old version. However, if you are using a project dependency, Gradle does not eliminate the duplication and you end up with duplicate classes in your code.

The solution to Library duplication is to provide a unified access to third-party libraries for the entire project. In the last section, we created a Common Library that provides a unified access to third-party libraries for the entire project. Gradle relies on the Common Library for all Common or required libraries, such as the Android Support Library, the Web Library, the image loading Library, etc. Since each component relies on the Common Library, Gradle does not need to rely on any other libraries, so we have a unified portal to rely on third-party libraries, adding, deleting, and upgrading library files is handled only in the Common library.

Here is the dependency configuration for the component build.gradle:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':common')
}Copy the code

When a component is merged into a main project, it is packaged into an ARR package. Therefore, the main project will rely on the Common library independently in the development mode of the component. When the main project is merged, it will rely on other components, so the Common library does not need to rely on other components.

dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) if (! isModule.toBoolean()) { compile project(':alert') compile project(':car') } else { compile project(':common') } }Copy the code

Another problem is that we need to configure properties such as compileSdkVersion, buildToolsVersion and defaultConfig in each component’s build.gradle. If you need to change the compileSdkVersion of your project, you will be in trouble. If you need to find all the build.gradle properties, you will need to set them all together. Like static constants in Java, a change takes effect everywhere. First we define the following code in our project (not component) build.gradle:

dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) if (! isModule.toBoolean()) { compile project(':alert') compile project(':car') } else { compile project(':common') } }Copy the code

The build.gradle code of the Common library will be slightly different from the build.gradle code of the component.

apply plugin: 'com.android.library'
android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
 defaultConfig {
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode rootProject.ext.versionCode
    versionName rootProject.ext.versionName
 }
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), '

proguard-rules.pro'        }    } } dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    //Android Support    compile "com.android.support:appcompat-v7:$rootProject.

supportLibraryVersion" compile "com.android.support:design:$rootProject.supportLibraryVersion" compile "Com. Android. Support: percent: $rootProject. SupportLibraryVersion"/request/network related the compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion" compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion" compile "com.github.franmontiel:PersistentCookieJar:$rootProject.

CookieVersion stable the compile "/ /" com. Making. Bumptech. Glide: glide: $rootProject. GlideVersion "the compile "com.orhanobut:logger:$rootProject.loggerVersion" compile "org.greenrobot:eventbus:$rootProject.eventbusVersion" compile "Com. Google. Code. Gson: gson: $rootProject. GsonVersion" / / unstable compile "com. Making. Mzule. Activityrouter: activityrouter:

$rootProject.routerVersion"    compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion" }Copy the code

This way you can change the compileSdkVersion, buildToolsVersion, defaultConfig or the versions of the dependent libraries directly in your project’s build.gradle file and the whole project will be changed.

5) Step 5: Jump across modules, which is the most important step for us

In the development of modular, we can’t use shows that the call to jump page, because we are one of the purposes of componentization is to solve the problem of the strong dependence on the modules, between components and component completely without any rely on, if I now from A component to jump to B component, and should carry parameters jump, at that time? And how to manage so many components is also a problem, at this time it is necessary to introduce the concept of “routing”.

I used an open source “routing” library in the project. Please click on ActivityRouter for github address. There will be a detailed introduction in the home page. In addition, Alibaba also open source a component routing, github address please click: ARouter; These two are ready to use, of course, some people may be curious about the component Router is what principle, how to develop their own, here has a detailed tutorial, you can go to learn: Android routing implementation.

Next we’ll talk about how to apply routing to our componentized project. First we’ll rely on the following code in our project (not component) build.gradle:

Buildscript {dependencies {classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'}}Copy the code

Why use Android-apt? Take a look at the explanation below, or do your own search:

Then add the following code to build.gradle for each component:

apply plugin: 'com. Neenbedankt. Android - apt' dependencies {the compile 'com. Making. Mzule. Activityrouter: activityrouter: 1.2.2' apt 'com. Making. Mzule. Activityrouter: the compiler: 1.1.7'}Copy the code

Next comes the configuration of androidmanifest.xml in the main project

< activity android:name="com.github.mzule.activityrouter.router.RouterActivity" android:theme="@android:style/Theme.NoDisplay"> < intent-filter> < action android:name="android.intent.action.VIEW" /> <  category android:name="android.intent.category.DEFAULT" /> < category android:name="android.intent.category.BROWSABLE" /> < data android:scheme="demo" /> < /intent-filter> < /activity>Copy the code

Next we need to declare each component in the Java directory as follows:

@Module("App")
public class AppModule {
}
@Module("Car")
public class Car { 
}Copy the code

Then declare all the components that need to be added to the main project in the main project’s Application:

@Modules({"App", "Car"}) public class DemoApplication extends BaseApplication { @Override public void onCreate() { super.onCreate(); }}Copy the code

At this point, the relationship between our components and the main project is established, and component declarations and additions and deletions are taken care of. Now, is there a jump of activities between components? We’ve done so much to prepare for the jump of activities.

First we add a comment to the target Activity that we want to jump to:

@Router("main")
public class MainActivity extends Activity {
    ...
 }Copy the code

This will open MainActivity with demo://main.

The ActivityRouter is the most advanced Router in the world.

6) Communication problems between Modules

If component B wants to tell component A to refresh the list, it needs to solve the communication problem between components, which can be solved by using EventBus and is not A complicated problem.

7) Resource name conflict

Because we split so many components, it is possible to have resource name conflicts when merging them into the main project, for example, component A and component B both define the same resource name. This problem can be easily solved by adding something like this to the build.gradle component:

ResourcePrefix "Component name _"Copy the code

The problem with setting this attribute is that all resource names must be prefixed with the specified string, otherwise an error will be reported, and the resourcePrefix value can only qualify resources in the XML, not images. All image resources still need to be manually modified. Therefore, I do not recommend using this method to solve resource name conflicts. In our project, the solution is to add a resource naming convention, which can avoid resource name conflicts as long as it is followed.

3. Conclusion of Android componentization Project

Here a simple componentized project has been built. Compared with a single project, componentized has obvious advantages: 1. Speed up compilation and improve development efficiency 2. Freely choose development framework (MVC /MVP/MVVM /) 3. 4. The code structure is clearer, reducing the maintenance difficulty of the project 5. Suitable for team development

Android componentized Demo:

https://github.com/guiying712/AndroidModulePattern

The first time to get blog updates, as well as more Android, source code analysis, the latest open source project recommendation, more valuable thinking, welcome to pay attention to my wechat public number, scan the qr code below or long press to identify the QR code