Now the development of big factory is basically componentized, so, not componentized friends can learn more. Today is to share with you about Android componentization specification development.

The original address: www.jianshu.com/p/7bc170d29…

The body of the

Componentized development has been going on for some time, and a new project will be started soon. For this purpose, the componentized development specifications used in the current project will be collated for use in the next project. The focus of this article is on the specification and project architecture, and only sample code examples are provided. If you don’t already know what componentization is and how to do it, please read this article first.

Android componentization: How can we learn and use componentization?

A Preliminary study on Android componentization

define

Component is a relatively independent function module in the Android project, is an abstract concept, module is a relatively independent code module in the Android project.

In the early days of component-based development, there was only one Module per component, causing a lot of code and resources to sink into common, which became bloated. Some articles say that a module is specifically built to store general resources. I feel that this is a palliative, until I see the practice of wechat Android modular architecture reconstruction. In the “general organization of modules” section, it mentioned that a module should have multiple projects, and then began to split modules in the project.

Generally speaking, a component has two modules, one lightweight module provides the interface methods that external components need to interact with this component and some resources that external components need, and the other heavyweight module completes the actual functions of the component and implements the interface methods defined by lightweight Modules.

Refer to module names for naming conventions of modules. In this section, module-API is used to represent lightweight modules, and module-impl is used to represent heavyweight modules.

Common components

Common is a special component that is not distinguished between lightweight and heavyweight. It is the lowest level component in a project. Basically, all other components depend on the Common component.

The architecture of a complete project is as follows:

Weak business logic code

What is weak business logic code? Simply put, there is some business logic, but this business logic is common to the other components of the project.

For example, in the common component integration network request library, create an HttpTool class, responsible for initializing the network request framework, define the network request method, implement the assembly of common request parameters and handle global common errors, for other components directly through the network request class can be used.

Such as defining interface base classes and handling common business logic such as accessing statistical analysis frameworks.

Code and resources that address loop dependencies

What are the code and resources to resolve loop dependencies? For example, module-a-API has a class C, module-b-API has a class D, in module-a-API you need to use D, in module-b-API you need to use C, This will cause module-a-API to rely on module-b-API, and module-b-API to rely on module-a-API, which will cause cyclic dependencies that will fail compilation in Android Studio.

The solution to cyclic dependencies is to sink one or both C and D into the common component, because both module-a-API and module-b-API depend on the common component. But the principle is that the less stuff sinks into the common component, the better.

The above example is code, resource files can also have this problem.

Module code structure

A component usually contains one or more function points. For example, for a user component, it has function points about interface, feedback, changing account password, etc. Create a path for each function point in the Module, and put the code to implement this function, such as Activity, Dialog, Adapter, etc. In addition, in order to centrally manage the internal resources of components and unify the coding habits, some general function paths are specially fixed. These paths include API, Provider, and tool.

In general, the code structure of Module is shown as follows:

api

This path holds all network request paths and methods used within the Module. A single class is usually sufficient, such as UserApi:

Object UserApi {/** * Get personal center data */ fun getPersonCenterData(): GetRequest { return HttpTool.get(ApiVersion.v1_0_0 + "authUser/myCenter") } }Copy the code

ApiVersion manages all API versions currently used in the project globally and should be defined under the API path of the common component:

object ApiVersion {
    const val v1_0_0 = "v1/"
    const val v1_1_0 = "v1_1/"
    const val v1_2_2 = "v1_2_2/"
}

Copy the code

entity

This path holds all entity classes (data classes returned by network requests) used within the Module.

All fields retrieved from the server are defined in the constructor, and the entity class should implement Parcelable and annotate @Parcelize. For fields that the client uses and defines itself, basically define them as ordinary member fields with the @ignoredonParcel annotation. If you need to pass a client-defined field between interfaces, you can define that field in the constructor, but you must specify that it is a client-defined field.

The following is an example:

@parcelize class ProductEntity(// Product name var name: String = "", // product icon var icon: String = "", // Product quantity (client defined field) var count: Int = 0) : Parcelable {@ignoredonparcel var isSelected = false}Copy the code

Where name and icon are fields fetched from the server, and count and isSelected are fields defined by the client itself.

event

This path drops event-related classes used within the Module. For projects that use EventBus or similar frameworks, place event classes. For projects that use LiveEventBus, place only one class, such as UserEvent:

Object UserEvent {/ * * * update user information successful event * / val updateUserInfoSuccessEvent: LiveEventBus.Event<Unit> get() = LiveEventBus.get("user_update_user_info_success") }Copy the code

Note: For projects that use LiveEventBus, event names must be prefixed with component names to prevent duplicate event names.

route

This path contains the interface path and jump methods used within the Module. A single class is usually sufficient, such as UserRoute:

Object UserRoute {const val ABOUT = "/user/ ABOUT "/ / FAQ (H5) private const val FAQ = "FAQ/" /** * Fun toAbout(): RouteNavigation {return RouteNavigation(ABOUT)} /** * jump toFAQ(H5) */ fun toFAQ(): RouteNavigation? { return RouteUtil.getServiceProvider(IH5Service::class.java) ? .toH5Activity(FAQ) } }Copy the code

Note: H5 interface links that jump inside components should also be written in routing classes.

provider

This path provides services to external Modules, usually using a single class. In module-API is an interface class, in module-impl is an implementation class of that interface class.

At present, ARouter is used as the componentized framework. For decoupling, it is encapsulated. The encapsulation example code is as follows:

typealias Route = com.alibaba.android.arouter.facade.annotation.Route

object RouteUtil {

    fun <T> getServiceProvider(service: Class<out T>): T? {
        return ARouter.getInstance().navigation(service)
    }
}

class RouteNavigation(path: String) {

    private val postcard = ARouter.getInstance().build(path)

    fun param(key: String, value: Int): RouteNavigation {
        postcard.withInt(key, value)
        return this
    }
    ...
}

Copy the code

The sample

Here’s how to jump to the about interface in the user component from the external Module and user-impl.

The preparatory work

Create routing class in user-impl, write routing and service routing about interface and jump to method about interface:

Object UserRoute {const val ABOUT = "/user/ ABOUT "/ /user component service const val USER_SERVICE = "/user/service" /** * */ navigation (): RouteNavigation {return RouteNavigation(ABOUT)}}Copy the code

Using routing in the About interface:

@Route(path = UserRoute.ABOUT)
class AboutActivity : MyBaseActivity() {
    ...
}

Copy the code

Define jump interface methods in user-API:

Interface IUserService: IServiceProvider {/** ** to (): RouteNavigation}Copy the code

Jump interface in user-impl

@Route(path = UserRoute.USER_SERVICE)
class UserServiceImpl : IUserService {

    override fun toAbout(): RouteNavigation {
        return UserRoute.toAbout()
    }
}

Copy the code
Interface jump

In user-impl you can jump directly to the about interface:

UserRoute.toAbout().navigation(this)

Copy the code

If module-a needs to jump to the about interface, configure dependencies in module-a first:

dependencies {
    ...
    implementation project(':user-api')
}

Copy the code

Use Provider in module-a to go to the about page:

RouteUtil.getServiceProvider(IUserService::class.java) ? .toAbout() ? .navigation(this)Copy the code
Module dependencies

The dependencies of each module are as follows:

Common: base library, third-party library User-API: common User-IMPl: common, user-API module-a: common, user-API App shell: Common, user-API, user-IMPl, module-a,...Copy the code

tool

This path holds the tool methods used within the Module. A single class is usually sufficient, such as UserTool:

Object UserTool {/** * whether this user is a member * @param gradeId member gradeId */ fun isMembership(gradeId: Int): Boolean { return gradeId > 0 } }Copy the code

cache

This path is used to store the cache methods used by the Module, usually a single class is sufficient, such as UserCache:

Object UserCache {// searchHistoryList: ArrayList<String> get() { val cacheStr = CacheTool.userCache.getString(SEARCH_HISTORY_LIST) return if (cacheStr == null)  { ArrayList() } else { JsonUtil.parseArray(cacheStr, String::class.java) ? : ArrayList() } } set(value) { CacheTool.userCache.put(SEARCH_HISTORY_LIST, Jsonutil.tojson (value))} private const val SEARCH_HISTORY_LIST = "user_search_history_list"}Copy the code

Note:

  1. The name of the cache Key must be prefixed with the component name to prevent duplicate cache keys.
  2. CacheTool.userCacheNot the cache of the user component, but the cache of the user, the current login account, each account will store a separate copy of data, without interference with each other. And that corresponds toCacheTool.globalCacheGlobal cache, where all accounts share one copy of data.

The difference between two modules

Everything in the module-API is required by the external component, or is required by both the external component and the module-impl. Everything else should be put in the module-impl. You should put the concrete implementation in module-IMPl, and just an interface method in module-API.

The following table lists what can be put into module-API during project development:

type Can you putmodule-api note
Function interface (Activity, Fragment, Dialog) Can’t throughproviderMethods to provide use
The base class interface Part of the can externalmoduleUse what you need. Leave the restmodule-impl
adapter Part of the can externalmoduleUse what you need. Leave the restmodule-impl
provider Part of the can Can only put interface classes, implementation classesmodule-impl
tool Part of the can externalmoduleUse what you need. Leave the restmodule-impl
API, Route, cache Can’t throughproviderMethods to provide use
entity Part of the can externalmoduleUse what you need. Leave the restmodule-impl
event Part of the can The use ofEventBusAnd similar framework projects, external components can be needed, the rest or putmodule-impl
For using theLiveEventBusThe items cannot be passedproviderMethods to provide use
Resource files and resource variables Part of the can Need to be inxmlThe ones used in the file are ok, the others passproviderMethods to provide use

Note: If only a utility class exists in module-impl, it is named xxTool. If both module-api and module-impl have utility classes, name them xxTool in module-API and xxTool2 in module-impl.

Component separate debugging

During the development process, in order to check the running effect, you need to run the entire App, which is quite troublesome, and other components that may depend on are also under development, so the App may not run the currently developed components. To this end, you can use a separate component debugging mode for development to reduce the interference of other components, and then switch back to library mode after development.

In component debug mode, you can add some additional code to facilitate development and debugging, such as adding an entry, Actvity, as the first interface when the component is run separately.

The sample

This section describes component-specific debugging in user-IMPl.

Add a variable isDebugModule to the gradle.properties file in the root directory of the project to control whether to debug the component separately:

IsDebugModule = falseCopy the code

Add the following code at the top of user-impl build.gradle to control user-impl switching between Applicaton and Library:

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

Copy the code

Create two folders in user-impl SRC /main: Release and debug, and put the androidmanifest.xml in library mode. Debug puts androidmanifest.xml, code, and resources in application mode, as shown below:

Configure the above created code and resource paths in user-impl build.gradle:

android {
    ...
    sourceSets {
        if (isDebugModule.toBoolean()) {
            main.manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            main.java.srcDirs += 'src/main/debug'
            main.res.srcDirs += 'src/main/debug'
        } else {
            main.manifest.srcFile 'src/main/release/AndroidManifest.xml'
        }
    }
}

Copy the code

Note: After the above configuration, in Library mode, the code and resources in debug are not merged into the project.

Finally, configure applicationId in user-impl build.gradle:

android { defaultConfig { if (isDebugModule.toBoolean()) { applicationId "cc.tarylorzhang.demo" } ... }}Copy the code

Note: If you encounter 65536 problems, add the following configuration to user-impl build.gradle:

android {
    defaultConfig {
        ...
        if (isDebugModule.toBoolean()) {
            multiDexEnabled true
        }
    }
}

Copy the code

Once all of this is done, change the value of isDebugModule to true and you can start debugging the user component separately.

Naming conventions

The module name

If the component name is a single word, use the suffix of the word + API or IMPl as the module name. If multiple words are lowercase, use the – character as the hyphen, and then append the suffix of API or IMPl as the module name.

The sample

The User component (User), whose Module names are user-API and user-impl; Membership card, whose modules are named membership- card-API and membership-card-impl.

The package name

Add the component name suffix to the application applicationId as the component base package name.

In the code of the package name module-api and module-impl directly use the base package name, but in the Android project androidmanifest.xml file package cannot be repeated, otherwise the compilation will not pass. So the package in module-impl uses the base package name, while the package in module-impl uses the base package name + API suffix.

Type package.BuildConfig is defined multiple times error if package is repeated.

The sample

The application applicationId is cc.taylorzhang.demo. For the user component (user), the basic package name of the component is cc.taylorzhang.demo.

Package name in the code The package name in androidmanifest.xml
user-api cc.taylorzhang.demo.user cc.taylorzhang.demo.userapi
user-impl cc.taylorzhang.demo.user cc.taylorzhang.demo.user

For membership card components (MembershipCard) more than words, its component base package called cc. Taylorzhang. Demo. The MembershipCard.

Resource files and resource variables

All resource files, such as layout files and images, should be prefixed with component names. All resource variables, such as strings and colors, should also be prefixed with component names to prevent duplicate resource names.

The sample

  • User Component (User), about the interface layout file named:user_activity_about.xml;
  • User Component (User), about the interface title string named:user_about_title;
  • Membership card component (MembershipCard), membership card details interface layout file, file name:membership_card_activity_detail;
  • Membership card component (MembershipCard), membership card details interface title character string, file name:membership_card_detail_title;

The name of the class

There is no need to add prefixes to class names, such as UserAboutActivity, because the main reason for adding prefixes to resource files and resource variables is to avoid the problem of overwriting resources by defining resources repeatedly. The package naming convention has already avoided the problem of class duplication.

Manage the App environment globally

App environment is generally divided into development, test and production environments. The network request address used in different environments is highly likely to be different, and even some UIs are different. Manual modification during packaging is easy to miss, resulting in unnecessary bugs. You should use buildConfigField to write the current environment into your App at package time, read environment variables in your code, and do different things for different environments.

The sample

The preparatory work

APP_ENV is configured for each buildType in the App shell build.gradle:

android {
    ...
    buildTypes {
        debug {
            buildConfigField "String", "APP_ENV", '\"dev\"'
            ...
        }
        release {
            buildConfigField "String", "APP_ENV", '\"release\"'
            ...
        }
        ctest {
            initWith release

            buildConfigField "String", "APP_ENV", '\"test\"'
            matchingFallbacks = ['release']
        }
    }
}

Copy the code

BuildType names cannot start with ‘test’. Android Studio returns ERROR: buildType names cannot start with ‘test’.

Create an App environment tool class in the common tool path:

Object AppEnvTool {/** development environment */ const val APP_ENV_DEV = "dev" /** test environment */ const val APP_ENV_TEST = "test" /** Production environment */ */ private var curAppEnv = APP_ENV_DEV fun init(env: APP_ENV_DEV fun init) String) {curAppEnv = env} /** Is currently in a development environment */ val isDev: Boolean get() = curAppEnv == APP_ENV_DEV /** Is currently in a test environment */ val isTest: Boolean get() = curAppEnv == APP_ENV_TEST /** Whether it is currently in production */ val isRelease: Boolean get() = curAppEnv == APP_ENV_RELEASE }Copy the code

Initialize the App environment tool class in Application:

class DemoApplication : Application() {override fun onCreate() {super.oncreate () // Initialize the App environment tool class appenvtool.init (buildconfig.app_env)... }}Copy the code

Use the App environment utility class

Here is how to use different network request addresses according to the App environment:

Object CommonApi {/ / API development environment to address private const val API_DEV_URL = "https://demodev.taylorzhang.cc/api/"/address/API test environment Private const val API_TEST_URL = "https://demotest.taylorzhang.cc/api/" / / API production address private const val API_RELEASE_URL = "Https://demo.taylorzhang.cc/api/" / / API address val API_URL = getUrlByEnv (API_DEV_URL API_TEST_URL, API_RELEASE_URL) / / H5 development environment address private const val H5_DEV_URL = "https://demodev.taylorzhang.cc/m/" / / H5 test environment address private Const val H5_TEST_URL = "https://demotest.taylorzhang.cc/m/" / / H5 production address private const val H5_RELEASE_URL = "Https://demo.taylorzhang.cc/m/" / / H5 address val H5_URL = getUrlByEnv (H5_DEV_URL H5_TEST_URL, H5_RELEASE_URL) private fun getUrlByEnv(devUrl: String, testUrl: String, releaseUrl: String): String { return when { AppEnvTool.isDev -> devUrl AppEnvTool.isTest -> testUrl else -> releaseUrl } } }Copy the code

packaging

Package through different commands and print the corresponding App environment package:

Gradlew Clean assembleCtest./ Gradlew Clean assembleReleaseCopy the code

Manage version information globally

With more modules in the project, it can be a pain to change the SDK versions used by third-party libraries and apps. A configuration file should be created for management, and the version set in the configuration file should be used elsewhere.

The sample

In the root directory of your project, create a config file config.gradle with version information:

Ext {compile_sdk_version = 28 min_sdk_version = 17 target_sdk_version = 28 arouter_compiler_version = '1.2.2'}Copy the code

At the top of the build.gradle file in the project root directory, import the configuration file with the following code:

apply from: "config.gradle"

Copy the code

After creating a Module, modify the build.gradle file in that Module, change the SDK version defaults to variables in the configuration file, add third-party dependencies as needed, and use variables in the $+ configuration file as the version of the third-party library:

android {
    ...
    compileSdkVersion compile_sdk_version

    defaultConfig {
        ...
        minSdkVersion min_sdk_version
        targetSdkVersion target_sdk_version
    }
}

dependencies {
    ...
    kapt "com.alibaba:arouter-compiler:$arouter_compiler_version"
}

Copy the code

confusion

Obfuscation files should not be defined centrally in the App shell, but should be defined in each Module for its own obfuscation.

The sample

Here are configuration user – impl confusion, in user – the impl build. The first consumer confusion that is configured in the gradle file:

android {
    defaultConfig {
        ...
        consumerProguardFiles 'proguard-rules.pro'
    }
}

Copy the code

Confusion over writing this module to proguard-rules.pro:

# entity class - keepclassmembers class cc. Taylorzhang. Demo. User. Entity. {* * *; }Copy the code

conclusion

Component-based development should follow the principle of “high cohesion, low coupling” and expose as few details as possible. To sum it up, code and resources that can be put in module-impl should be put in module-IMPl, because code isolation cannot be put in module-IMPl should be put in module-API. Finally, we put it in common because of loop dependencies.

The last

If you want to learn about Android componentization, you can check out the previous two articles:

Android componentization: How can we learn and use componentization?

A Preliminary study on Android componentization

Android componentization: Get APP, code isolation is not difficult to initialize components in order

Or, we can also go to B station to see this old woman Lord handling video: Android development camel

Long wind and waves will sometimes, straight sail to the sea. Come on ~