Android componentized architecture

First language

Time is flowing, it is already March of 2021, and I am full of spirits. Continue to work hard in the new year.

Introduction

In project development, the common code is extracted into common_module, some individual functions are encapsulated into lib_module, and modules are divided according to business, and team members develop their own modules.
However, with the iteration of the project, more and more functions, after adding some business modules, the situation of mutual calls will increase, and the coupling between the business modules will be very serious, resulting in the code being difficult to maintain and the scalability is poor. Componentization is born.
Component basis: multi-module division of business and basic functions.
Components: A single functional component, such as adaptation, payment, routing components, etc., can be extracted separately to form an SDK.
Module: Independent business module, such as live broadcast, home page module, etc. The module may contain many different components.
Basic componentized architecture

Features

  1. Avoid re-creating wheels, saving development and maintenance costs.
  2. Arrange manpower reasonably through components and modules to improve development efficiency.
  3. Different projects share one component or module to ensure the unity of technical solutions.
  4. In the future, plug-in will be prepared for a common set of underlying models.

Component programming

Componentized Application

If the function module has an Application, and the main module does not have a custom Application, it will naturally refer to the Application of the function module.
If the function module has two custom Applications, compilation errors will occur and conflicts need to be resolved. You can use the tools:replace="android:name"solution, because App compilation will only allow one Application to be declared in the end.

Communication between components

The modules in the component are independent of each other, there is no dependency, and no information cannot be transmitted without dependency. At this time, you need to rely on the basic layer (CommonModule), the modules of the component layer all depend on the CommonModule, which is the basis for information exchange between modules.
Activity, Fragment, and Service information transmission in Android is more complicated, and it is time-consuming and unsafe to realize message transmission in the form of broadcast, resulting in an event bus mechanism. It is an implementation of the publish-subscribe model. It is a centralized event processing mechanism that allows different components to communicate with each other without interdependence, achieving a decoupling purpose.

Third-party bus framework
  • EventBus
    EventBus is a publish/subscribe message bus optimized on the Android side, which simplifies the communication between components in the application and between components and background threads. For specific usage, please refer to my blog: EventBus of Android Event Bus .
  • RxBus
    RxBus is a mode of communication between components derived from RxJava responsive programming. At present, project development network requests are implemented using the Retofit+RxJava framework. For specific usage methods, please refer to my blog: Use of Android RxJava ; Retrofit .
Compared

In terms of thread scheduling, RxJava's thread scheduling is more excellent, and it is better than Eventbus to write code through a variety of operators, chain type, but because it does not use the reflection mechanism, the operating efficiency is lower than EventBus.

to sum up

In actual project development, communication events should be placed in CommonModule, and CommonModule also needs to rely on the bus framework. However, when different modules are added or deleted, message models need to be added or deleted, which makes the entire architecture of the event bus very bloated and complicated, which violates the principle of componentization. The solution is to extract an event bus module, CommonModule depends on this module, the message model is in the event bus module.

Jump between components

In componentization, the two functional modules are not directly dependent, but are indirectly dependent through CommonModule. Generally, an activity jumps to another activity, and startActivity is used to send an intent, but the activity of other modules cannot be referenced. Jump can be realized through implicit Action. It should be noted that when removing the module, the jump must also be removed, otherwise a crash will occur.

ARouter route jump

Implicit Action is not the best way to jump, ARouter appears at this time.
ARouter is an open source routing framework developed by Alibaba's Android technical team to help componentize Android apps, and supports routing, communication, and decoupling between modules.
Github address: https://github.com/alibaba/ARouter

  • Use
    First add dependency in CommonModule:
    implementation 'com.alibaba:arouter-api:x.x.x'
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'

Then the annotationProcessor will use the javaCompileOptions configuration to get the name of the current module, and add it to the defaultConfig property of the build.gradle of each module:

android {
    
    
    defaultConfig {
    
    
        ...
        javaCompileOptions {
    
    
            annotationProcessorOptions {
    
    
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

The dependencies attribute of each module needs to be referenced by ARouter apt, otherwise the index file cannot be generated in apt, and the jump cannot be successful.

dependencies {
    
    
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
}

Initialize in Application:

if (isDebug()) {
    
               
    ARouter.openLog();   
    ARouter.openDebug();   
}
ARouter.init(mApplication); 

Take Activity jump as an example. First, you need to add the annotation Route in the jump Activity, 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";

When you need to jump to the Activity, use the following, the build parameter is the jump Activity path.

ARouter.getInstance().build(RouterPath.LOGIN_PAGE).navigation();

For specific usage, please refer to the official Chinese documentation: https://github.com/alibaba/ARouter/blob/master/README_CN.md

Componentized storage

There are five native Android storage methods, which are also completely universal in componentization.
The popular database in componentization is Room in the Jetpack suite. It completes operations such as database creation, addition, deletion, modification, and checking in the form of annotations. Simple and efficient to use.
In the componentized design, decoupling is taken into account, and the database layer is separated into a module. The operations on the database are all in this module and depend on the CommonModule.

Componentized rights management

In the AndroidManifest.xml of each module, we can see the permission application of each module, which will eventually be merged into the root AndroidManifest.xml file.
In the component development, we put the normal level permissions in the CommonModule, and apply for the dangerous level permissions in each module separately. The advantage of this is to remove the dangerous level permissions when adding or removing a module. Maximum decoupling.

Dynamic permission framework

RxPermission is an Android dynamic permission application framework based on RxJava.
Github address: https://github.com/tjianssbruyelle/RxPermissions .

    public void initPermissions(String[] permissions, PermissionResult permissionResult) {
    
    
        if (rxPermissions == null) {
    
    
            rxPermissions = new RxPermissions(this);
        }
        rxPermissions.requestEachCombined(permissions)
                .subscribe(permission -> {
    
    
                    if (permission.granted) {
    
    
                        permissionResult.onSuccess();
                    } else if (permission.shouldShowRequestPermissionRationale) {
    
    
                        permissionResult.onFailure();
                    } else {
    
    
                        permissionResult.onFailureWithNeverAsk();
                    }
                });
    }

The combination of RxPermission and RxJava is very streamlined, simple and practical.

Componentized resource conflict
AndroidMainfest conflict

The app:name attribute of Application is referenced in AndroidMainfest, and tools:replace="android:name" is used to declare that Application is replaceable in case of conflict.

Packet conflict

When a package conflict occurs, use the gradle dependencies command to view the dependency directory tree. The dependencies marked with * indicate that the dependencies are ignored. Because there are other top-level dependencies that also depend on this dependency, you can use exclude to exclude the dependency, for example:

 androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2', {
    
    
        exclude group: 'com.android.support', module: 'support-annotations'
    }
Resource name conflict

In multi-module development, there is no guarantee that the names of all resources in multiple modules are different. The rule for selecting the same resource name is that the later compiled module will overwrite the content in the resource field of the previously compiled module, and the same will cause resource reference errors. The problem. There are two solutions: the
first: rename resources when conflicts occur.
The second: gradle's naming prompt mechanism, using the resourcePrefix field:

android {
    
    
	resourcePrefix "组件名_"
}

All resource names must be prefixed with the specified string, otherwise an error will be reported, but resourcePrefix cannot limit image resources. For image resources, you 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. It can delete useless classes and comments and optimize bytecode files to the greatest extent.
Obfuscation will delete useless resources of the project and effectively reduce the size of the apk installation package.
Confusion increases the difficulty of reverse engineering and makes it safer.
Confusion has four operations: 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 comes with a custom obfuscation file of proguard-rules.pro, and each module can have its own obfuscation rules.
In componentization, if each module uses its own obfuscation, repeated obfuscation will occur, causing the problem of not being able to query resource files. We need to ensure that only one obfuscation is required when the apk is generated.
Solution: Put the fixed third-party library obfuscation into CommonModule, and put the unique reference library obfuscation of each module in its own proguard-rules.pro, and finally put Android basic attributes in the app's proguard-rules.pro Obfuscation statement, such as four major components and global obfuscation configuration. The confusion and decoupling work can be completed to the maximum extent.

Componentized multi-channel

When the user terminal and management terminal need to be generated during project development, or some versions do not require payment, sharing, etc., we do not need to embed these modules, and at the same time can reduce the business volume and package capacity.
When we need to export multiple apps, the maintenance and development costs will increase. How to reduce the development costs and to understand the coupling, we need to use multiple channels. E.g:

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 multiple channels through productFlavors, and manifestPlaceholders set different attributes of different channels. These attributes can only be used when declared in AndroidMainfest. Setting xxxImplementation can configure modules that need to be referenced by different channels. You can find Build Variants in the left sidebar of Android Studio to choose different Active Build Variant.
For different channels that need to introduce new classes or files, you can create different channel folders under the project directory, put the files in them, and use them for each.
multi-channel

Gradle optimization

Gradle is essentially an automated build tool that declares project settings based on Groovy's Domain Specific Language (DSL). When Android Studio builds the project, it uses the plug-in written by gradle to load the project configuration and compile files.
In componentization, each module has a build.gradle file, and the build.gradle file of each module has some required attributes. 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 and basic Gradle configuration, create a version.gradle file, write some variables, and add them in the build script of the project’s build.gradle

apply from :"versions.gradle"

Similar to the way of referencing static variables to refer to properties, you can also configure the warehouse used by the project in version.gradle in a unified manner. Just add it in project.gradle.

ext.deps = [:]

def versions = [:]
versions.gradle = "4.0.1"
versions.appcompat = "1.2.0"
versions.constraintlayout = "2.0.4"
versions.junit = "4.12"
versions.ext_junit = "1.1.2"
versions.espresso_core = "3.3.0"
versions.multidex = "1.0.3"
def build_versions = [:]

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

def view = [:]
view.constraintlayout = "androidx.constraintlayout:constraintlayout:$versions.constraintlayout"
view.recyclerview = "androidx.recyclerview:recyclerview:$versions.recyclerview"
view.glide = "com.github.bumptech.glide:glide:$versions.glide"
view.glide_compiler = "com.github.bumptech.glide:compiler:$versions.glide_compiler"
view.circleimageview = "de.hdodenhof:circleimageview:$versions.circleimageview"
view.gif_drawable = "pl.droidsonroids.gif:android-gif-drawable:$versions.gif_drawable"
view.material = "com.google.android.material:material:$versions.material"
deps.view = view

def addRepos(RepositoryHandler handler) {
    
    
    handler.google()
    handler.jcenter()
    handler.flatDir {
    
     dirs project(':lib_common').file('libs') }
    handler.maven {
    
     url "https://jitpack.io" }
}

ext.addRepos = this.&addRepos

Then we just need to use it like this in the module's build.gradle.

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 parameter variable configuration is unified, so that the project will not reference multiple different versions of the Android tool library, and the configuration is unified to avoid increasing the apk capacity.

Debug optimization

Componentization supports starting a single module as an App, and then using it for debugging and testing, ensuring that individual modules can be debugged separately.
Need to change:

apply plugin: 'com.android.library'——>apply plugin: 'com.android.application'

Create a debug folder in src. The debug folder is used to place the AndroidMainfest.xml files, java files, res files, etc. required for debugging, and the default startup activity needs to be set.
We can set an isModule variable as the switch of integrated development and component development mode, which can be judged in the module's build.gradle as follows:

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

At the same time, all files in the debug folder need to be excluded in the integrated development mode.

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 separately debugged.

dependencies {
    
    
    if (!isModule.toBoolean()) {
    
    
       implementation project(path: ':module_my')
    }
}

to sum up

The componentization practice in the Android project can improve reusability and reduce coupling. This article mainly summarizes the common usage scenarios of componentization in the project, and more related scenarios will be summarized in the project development.

Guess you like

Origin blog.csdn.net/yang_study_first/article/details/111658088