Android Component Based Architecture
Android Component Based Architecture
First Words
Time is like flowing water, it is already March 2021, and I am full of spirits. Continue to work hard in the new year.
Brief introduction
In project development, the common code is extracted into the common_module, some individual functions are encapsulated into the lib_module, and then the
modules are divided according to the business, and the team members develop their own modules.
However, with the iteration of the project, more and more functions are added, and after some business modules are added, the number of mutual calls will
increase, and the coupling between various business modules will be very serious, resulting in difficult maintenance of the code and poor scalability.
Componentization is the application.
Component-based foundation: Multi-module divides services and basic functions.
Components: Single functional components, such as adaptation, payment, and routing components, can be extracted separately to form an SDK.
Module: Independent business module, such as live broadcast , home page module, etc. A module may contain several different components.
peculiarity
2. Reasonable manpower arrangement through components and modules to improve development efficiency.
3. Different projects share a single component or module to ensure the uniformity of the technical solution.
4. In the future, plug-in will be used to prepare for a common set of underlying models.
Component-based programming
Componentized Application
If the functional module has an Application, the main module does not have a custom Application, and the Application of the functional module is naturally
referenced.
If a functional module has two custom Applications, a compilation error will occur and the conflict needs to be resolved. This can be solved because App
compilation will only allow one Application to be declared in the end. tools:replace="android:name"
Communication between components
The modules in the components are independent of each other and have no dependencies, without which information cannot be transmitted. In this case, the
base layer (CommonModule) is needed, and the modules of the component layer depend on the CommonModule, which is the basis for information exchange
between modules.
In Android, Activity, Fragment and Service information transmission is complex, and the message transmission in the form of broadcast is time-consuming and
insecure, resulting in an event bus mechanism. It is an implementation of the publish-subscribe pattern. It is a centralized event processing mechanism that
allows different components to communicate with each other without relying on each other, achieving a kind of decoupling purpose.
EventBus EventBus is an Android-optimized publish/subscribe message bus that simplifies communication between components within an application and
between components and background threads. For more information, please refer to my blog: EventBus for Android Event Bus.
RxBus RxBus is a communication mode between components derived from RxJava reactive programming, and the current project development network
requests are implemented using Retofit+RxJava framework, for specific usage methods, please refer to my blog: Android RxJava Usage; Retrofit。
contrast
In terms of thread scheduling, RxJava's thread scheduling is better, and it is better than Eventbus to write code through multiple operators and chaining, but
because it does not use the reflection mechanism, it is less efficient than EventBus.
summary
In actual project development, communication events need to be placed in the CommonModule, which also needs to rely on the bus framework. However,
different modules need to add or delete message models, which makes the entire architecture of the event bus very bloated and complex, which violates the
principle of componentization. The solution is to extract an event bus module, on which CommonModule depends, and the message model is in the event bus
module.
In componentization, there is no direct dependency on the two functional modules, but indirect dependencies are provided through the CommonModule.
Generally, one activity jumps to another activity, and startActivity is used to send an intent, but it cannot reference the activities of other modules. You can use
an implicit action to redirect. It should be noted that when removing the module, the jump must also be removed, otherwise a crash will occur.
Implicit Action is not the best way to jump, and this is where ARouter comes in.
ARouter is an open-source routing framework used by Alibaba's Android technology team to help Android apps transform into components, supporting routing,
communication, and decoupling between modules.
Github address: https://github.com/alibaba/ARouter
use
Start by adding dependencies in the CommonModule:
implementation 'com.alibaba:arouter-api:x.x.x'
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
The annotationProcessor then uses the javaCompileOptions configuration to get the name of the current module, adding it to the defaultConfig property of
build.gradle for each module:
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
The dependencies property of each module needs to be referenced by the ARouter apt, otherwise the index file cannot be generated in the apt, and the
redirection will not be successful.
dependencies {
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
}
Initialize in Application:
if (isDebug()) {
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(mApplication);
For example, you need to add the annotation Route to the activity, and path is the path
@Route(path = RouterPath.LOGIN_PAGE)
public class LoginActivity extends BaseActivity<ActivityLoginBinding> {}
//路由跳转尽量统一管理,可以module路径命名。
String SURROUNDING_PAGE = "/surrounding/ui";
String TRAVEL_PAGE = "/travel/ui";
String CITY_SERVICE_PAGE = "/city_service/ui";
If you want to jump to an activity, use the following parameter and set the build parameter to the path of the activity.
ARouter.getInstance().build(RouterPath.LOGIN_PAGE).navigation();
For specific use, please refer to the official Chinese documentation: https://github.com/alibaba/ARouter/blob/master/README_CN.md
Component-based storage
There are five types of Android native storage, and they are also completely common in componentization.
A popular database in componentization is Room in the Jetpack suite. It completes operations such as creating, adding, deleting, modifying, and querying
databases in the form of annotations. Simple and efficient to use.
The component-based design takes into account decoupling, and the database layer is separated into a module, and the operations on the database are all in
this module, and it depends on the CommonModule.
In the AndroidManifest.xml of each module, we can see the permission requests of each module, which will eventually be merged into the root AndroidManifest
.xml file.
In component-based development, we put the normal level permissions in the CommonModule, and apply for dangerous permissions in each module, which
has the advantage of removing the dangerous level permissions when adding or removing a module, so as to achieve the greatest degree of decoupling.
AndroidMainfest references the app:name property of Application, and uses tools:replace="android:name" to declare the Application replaceable in case of
conflict.
Package conflicts
When a package conflict occurs, run the gradle dependencies command to view the dependency tree, and if the dependency is marked with an asterisk, the
dependency is ignored. Since there are other top-level dependencies that also depend on this dependency, you can use exclude dependencies, for example:
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2', {
exclude group: 'com.android.support', module: 'support-annotations'
}
In multi-module development, it is impossible to guarantee that all resources in multiple modules are named differently, and the rule for selecting the same
resource name is that the post-compiled module will overwrite the content in the resource field of the previously compiled module, and the same will cause the
resource reference error. There are two solutions:
First, rename resources when they are in conflict.
The second is gradle's naming hint mechanism, which uses the resourcePrefix field:
android {
resourcePrefix "组件名_"
}
All resource life must be prefixed with the specified string, otherwise an error will be reported, but resourcePrefix cannot limit image resources, and image
resources also need to manually modify the resource name.
Componentized obfuscation
Android Studio uses ProGuard for obfuscation, which is a tool for compressing, optimizing, and obfuscating Java bytecode files, removing useless classes and
comments, and maximizing the optimization of bytecode files.
Obfuscation removes the useless resources of the project, effectively reducing the size of the APK installation package.
Obfuscation makes reverse engineering more difficult and safer.
There are four types of obfuscation: Shrinking, Optimization, Obfuscation, and Preverification.
buildTypes {
release {
//是否打卡混淆
minifyEnabled false
//是否打开资源压缩
shrinkResources true
设置proguard的规则路径
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
After each module is created, it will come with a proguard-rules.pro custom obfuscation file, and each module can have its own obfuscation rules.
In componentization, if each module uses its own obfuscation, repeated obfuscation will occur, resulting in the problem that the resource file cannot be
queried. We need to make sure that the APK is generated only once.
Solution: Put the fixed third-party library obfuscation into the CommonModule, the unique reference library obfuscation of each module in its own proguard-
rules.pro, and finally put the Android basic attribute obfuscation declaration in the app's proguard-rules.pro, such as the configuration of the four major
components and global obfuscation. Obfuscation and decoupling can be completed to the greatest extent.
Componentized multi-channel
When the user side and management side need to be generated in the project development, or some versions do not need to be paid and shared, etc., we do
not need to embed these modules, and at the same time, we can reduce the business volume and package capacity.
When we need to output multiple apps, the maintenance and development costs will increase, how to reduce the development costs, and decouple, we need
to use multiple channels. For example:
productFlavors {
phone {
applicationId "com.zdww.enjoyluoyang"
manifestPlaceholders = [name:"用户端",icon:"@mipmap/logo"]
}
terminal {
applicationId "com.zdww.enjoyluoyang_terminal"
versionCode 2
versionName "1.1.0"
manifestPlaceholders = [name:"管理端",icon:"@mipmap/logo"]
}
}
phoneImplementation project(path: ":module_my")
We set up multi-channel through productFlavors, manifestPlaceholders set different properties for different channels, these properties can only be used in
AndroidMainfest, and set xxxImplementation to configure the modules that need to be referenced by different channels. In Android Studio, you can find Build
Variants in the left sidebar, and select the different Active Build Variants.
If you need to introduce new classes or files for different channels, you can create a new folder for different channels in the project directory, and put the files
into them for their own use.
Channels
Gradle optimization
Gradle is essentially an automated build tool, based on Groovy's domain-specific language (DSL) to declare project settings, Android Studio uses the plugin
written in gradle to load project configuration and compile files when building a project.
In componentization, each module has a build.gradle file, and the build.gradle file of each module has some required attributes, and the same Android project
requires these attributes to be consistent in different modules, such as compileSdkVersion, etc., if the references are inconsistent, the attributes will not be
merged and introduced into the project, which will cause duplication of resources and reduce compilation efficiency.
There must be a unified, basic Gradle configuration, create a version.gradle file, write some variables, and add buildscript under the project's build.gradle
In the same way as referencing static variables, you can also configure the repository used by your project in version.gradle. Just add it in project.gradle.
ext.deps = [:]
build_versions.compileSdk = 29
build_versions.minSdk = 19
build_versions.targetSdk = 29
build_versions.versionCode = 11
build_versions.versionName = "1.4.5"
build_versions.application_id = "com.example.yhj"
build_versions.gradle = "com.android.tools.build:gradle:$versions.gradle"
ext.build_versions = build_versions
ext.addRepos = this.&addRepos
Then we just need to use it like this under the build.gradle of the module.
android {
compileSdkVersion build_versions.compileSdk
defaultConfig {
minSdkVersion build_versions.minSdk
targetSdkVersion build_versions.targetSdk
versionCode build_versions.versionCode
versionName build_versions.versionName
api deps.android.appcompat
api deps.view.constraintlayout
//glide
api deps.view.glide
annotationProcessor deps.view.glide_compiler
In this way, the configuration of parameter variables is unified, so that the project will not be referenced to multiple different versions of Android tool libraries,
and the configuration is unified to avoid increasing the apk capacity.
Debugging optimization
Componentization supports a single module to be launched as an app, and then used for debugging and testing, ensuring that individual modules can be
debugged separately.
Changes to be made:
Create a debug folder in src, which is used to place AndroidMainfest .xml files, java files, res files, etc. required for debugging, and you need to set the default
startup activity.
We can set an isModule variable to switch between integrated development and component development modes, which can be determined in the module's
build.gradle like this:
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
In the integrated development mode, you need to exclude all files in the debug folder.
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//集成开发模式下排除debug文件夹中的所有文件
java {
exclude 'debug/**'
}
}
}
}
The build.gradle of the original app needs to remove the module dependencies that have been debugged separately.
dependencies {
if (!isModule.toBoolean()) {
implementation project(path: ':module_my')
}
}
summary
This article mainly summarizes the common use scenarios of componentization in the project, and more related scenarios are summarized in the project
development.