Modular Architecture and Componentization for Youzan Android Applications
Youzan refactored its Android Micro Mall and Retail apps from a monolithic codebase into a service‑oriented, componentized architecture where each business module inherits a BaseModule with its own lifecycle, common utilities are extracted into lightweight libraries, and a custom Gradle plugin enables independent building, publishing, and rapid iteration.
1. Overview
The main work of Youzan's mobile side is on the two core business lines "Youzan Micro Mall" and "Youzan Retail". As the SaaS business grows, the client side continuously iterates to support more and more features.
With this rapid business growth, the overall mobile architecture is constantly being adjusted to ensure development efficiency and fast business iteration.
This article introduces the ideas and implementation of componentization for the Youzan Micro Mall Android project.
1.1 Current Situation
The client architecture has evolved from an "All‑IN‑ONE" mode (all code in a single app) to a multi‑module project structure:
1.2 Pain Points
Complex Common module with unclear dependencies, leading to redundant code and unmaintainable business logic.
Increasing number of business modules makes packaging time explode, slowing development rhythm.
Business modules are tightly coupled with the upper (App shell) and lower (Common) layers.
Business modules lack independent lifecycle, making isolation and control chaotic.
1.3 Problems to Solve
Lightweight Common module: extract common low‑level and UI components into separate dependencies.
Service‑oriented mobile business: decouple existing business, expose interfaces, and enable cross‑module calls.
Configurable single‑module or multi‑module packaging to avoid full‑app builds for every change.
Manage dependencies and publishing of business modules.
2. Architecture Adjustments
Although we have been working on modular development, the current modular framework is not thorough enough:
Modules are only a project‑structure concept; there is no logical module concept.
Modules have no lifecycle control.
Common services are centralized in the Common module.
Exposed services are opaque; they directly depend on internal code.
Modules cannot be packaged independently; any code change requires a full build.
To solve these issues, we need to adjust the existing architecture.
2.1 Module Abstraction
We abstract module functionality into base classes, forming a modular support component that provides:
Each business module implements its own module class inheriting from BaseModule and registers it in the App shell.
Modules have a lifecycle similar to Activities (register services, initialize data, etc.).
Modules can register services that are added to the App’s service center during registration.
2.2 Decentralizing the Common Business
Many apps eventually create a large "Common" module that becomes a catch‑all storage for utilities, UI components, shared business classes, and core libraries. This leads to:
Redundancy – duplicate utilities are added when needed.
High maintenance cost – all shared logic resides in Common, making impact analysis difficult.
2.2.1 What Exists in Common?
Utility classes
Shared UI components
Business classes used by multiple modules
Core component wrappers (image library, network library, WebView)
Base classes (BaseActivity, BaseFragment, etc.)
2.2.2 Solution
Extract shared business modules upward into individual service‑oriented modules.
Abstract basic components into a separate component.
Move foundational classes into a core library without business logic.
2.3 Business Module Service‑orientation
“Service‑oriented” is a term often used on the server side. In the client, it means dividing the app into multiple modules that interact by providing services.
Typical Android module dependencies are:
Module A directly depends on Module B and calls its code.
Common module holds shared code; both A and B depend on Common.
2.3.1 Implementation of Service Dependency
Backend services use Dubbo RPC; clients depend only on the API module (interfaces and data structures).
Mobile side can adopt a similar approach – e.g., a product module provides an API layer that exposes data structures and services.
2.3.2 API Layer Implementation
Service exposure methods:
Protocol style: e.g., "app://order/detail/get?id=100" with JSON data, similar to an HTTP request.
Interface style: like Dubbo, the order module provides a Maven dependency containing data and service interfaces.
2.3.3 Issues with Protocol Style
Changing the service provider requires manual updates across all callers, lacks version management, and needs manual data conversion.
2.3.4 Issues with Interface Style
Requires an additional Maven dependency (AAR) which adds publishing and dependency‑management overhead, but this can be handled by Gradle plugins.
We finally chose the interface approach because it offers better stability and version control; Gradle plugins can automate the publishing cost.
2.4 Basic Component Abstraction
2.4.1 Existing Basic Components
Many basic components (account library, network library, image loader, Web container, etc.) are currently packaged inside Common, causing:
Heavy Common module.
Strong coupling between business modules and basic components, making cross‑team component reuse difficult.
High upgrade cost; API changes require modifications in every consumer.
2.4.2 Implementation Idea
Extract frequently used basic components into an independent abstraction layer defining interfaces (image loading, Web container, JsBridge, account, etc.).
Provide concrete implementations in a separate dependency that the App registers; business modules depend only on the abstraction.
2.4.3 Dependency Structure
2.5 Single / Multi‑module Packaging
As business volume and complexity increase, and third‑party components are added, the codebase grows, leading to slow builds. Example:
After developing a feature in a product module, you must build the whole app to test it, which can take 5‑10 minutes per build. Repeating this ten times a day is painful.
Our solution supports building a single module or a selected set of modules by using a custom Gradle plugin that dynamically changes the Android Gradle plugin type during compilation.
Tests show a full clean build takes about 4 minutes, while a single‑module clean build takes about 1 minute – a reduction of over 70%.
2.6 Architecture Diagram
3. Implementation Plan
Our solution provides three basic component dependencies and one Gradle plugin:
modular‑core: provides lifecycle and module service registration.
modular‑support: abstracts third‑party package interfaces.
modular‑support‑impl: default implementations for third‑party interfaces.
modular‑plugin: generates API directories, creates the runtime environment, and manages module publishing.
3.1 modular‑core
3.1.1 Module Class Implementation
Business module classes need to inherit from BaseModule:
public class ModuleA extends BaseModule {
@Override
public void onInstalled() {
registerBusinessService(ModuleAService.class, new CachedServiceFetcher() {
@Override
public ModuleAService createService(@NotNull ModularManage manager) {
if (service == null) {
service = new ModuleAServiceImpl();
}
return service;
}
});
}
}3.1.2 Module Lifecycle
Modules have the following lifecycle callbacks:
onInstalled() – called when the module is registered in the App.
onCreate() – called the first time the module’s Activity starts.
onStart() – called when the module starts.
onStop() – called when no Activity of the module remains in the back stack.
3.1.3 Lifecycle Implementation
Lifecycle capture and listening rely on Android Architecture Components’ Lifecycle library. Modules register Activities and globally listen to Lifecycle.Event to track module Activity states.
3.2 modular‑plugin
The plugin extends Gradle, providing highly extensible features that simplify component development without intrusive changes.
3.2.1 Gradle Lifecycle
Many extension functions are implemented by hooking into various stages of Gradle’s lifecycle. The overall lifecycle is illustrated below:
3.2.2 Single‑module Packaging
Packaging an Android module as an APK requires:
Proper AndroidManifest.xml configuration (application tag).
Main Activity configuration.
3.2.2.1 Implementation Principle
Automatically generate a dedicated Application class for the module.
Read and modify the module’s AndroidManifest to make it packable as an app.
Dynamically change the SourceSet during packaging to use the generated files.
Dynamically switch the plugin type between com.android.application and com.android.library.
3.2.2.2 Gradle Configuration Example
modular {
// Module package name
packageName = "com.youzan.ebizcore.plugin.demoa"
app {
// Single‑module packaging switch
asApp = true
// Name of the generated app
appName = "Module A"
// Entry Activity
launchActivity = "com.youzan.ebizcore.plugin.demoa.ModuleAActivity"
requires {
// Dependencies needed only for single‑module packaging
require "com.squareup.picasso:picasso:2.3.2"
}
}
}3.2.2.3 Generating the Runtime Environment
Running the createApp task automatically generates the required directory structure, e.g.:
./module_a
--src
----main
------app # generated app directory
--------java # generated Application class
--------res # generated resources
--------AndroidManifest.xml # generated manifest3.2.2.4 Executing the Packaging Task
Running the runAsApp task builds the module into an APK and installs it on the device. If the module has contextual dependencies (e.g., login), they can be added to the requires section.
3.2.3 Module API Management
The module’s API layer provides interfaces and data structures that can be referenced internally. When exposing the module externally, it is packaged as an AAR and uploaded to Maven. The plugin provides two tasks to streamline development and publishing.
3.2.3.1 Adding API Configuration in build.gradle
modular {
packageName = "com.youzan.ebizcore.plugin.demoa"
api {
// Enable API support (affects task generation)
hasApi = true
// Name of the API service class
apiService = "ModuleAService"
requires {
require "com.google.code.gson:gson:2.8.2"
}
}
}3.2.3.2 Generating API AAR
Running the createApi task generates the API source set, e.g.:
./module_b
--src
----main
------service # generated service directory for API interfaces and data objects
--------java # generated Application class
--------AndroidManifest.xml # generated manifest for AAR3.2.4 Module Publishing
Publishing uses the maven-publish plugin. Developers only need to configure the publishing parameters:
modular {
publish {
active = true
repo = "../release"
groupId = "com.youzan.ebizmobile.demo"
artifactId = "modular-a"
moduleVersion = "0.1.4"
apiVersion = "0.1.5"
userName = ""
password = ""
}
}Running the uploadModule task first builds and uploads the API AAR, then removes API classes from the module’s source set and adds the published API as a Maven dependency before packaging the business module.
3.3 modular‑support
3.3.1 Basic Component Abstraction Example
For an image component, we can define an interface like:
interface IImageLoadSupport {
fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String)
fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, @DrawableRes drawableId: Int)
fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String, callback: ImageLoadCallback<IMAGE>)
fun imagePicker(activity: Activity?, selectedImgUris: List<Uri>)
fun onImagePickerResult(requestCode: Int, resultCode: Int, intent: Intent?): List<String>?
}3.3.2 Implementation of Basic Components
Implementations are registered in the App. If a single‑module needs Support functionality, a default implementation can be provided and stored in a global map:
fun <SUPPORT : Any, SUPPORTIMPL : SUPPORT> registerProvider(
supportCls: Class<SUPPORT>,
provider: SupportProvider<SUPPORTIMPL>
) {
synchronized(Lock) {
supportsProviderMap[supportCls] = provider
if (supportsMap.containsKey(supportCls)) {
supportsMap.remove(supportCls)
}
}
}4. Planning
Introduce dependency injection in Modular‑Support to hide instance acquisition.
Provide native capabilities for Weex, RN, H5, Flutter via Modular‑Support.
Further reduce packaging time and increase flexibility with Modular‑Plugin.
Continue optimizing management and dependency packaging functions.
5. Conclusion
Componentization is an evolving journey. Continuous architectural adjustments aim to improve development efficiency and project stability. The ideas and solutions presented here hope to inspire teams exploring modularization.
Further Reading
1. A Brief Talk on Android Dex Files
2. Youzan's Weex‑Based Mobile Development Framework
-The End-
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Youzan Coder
Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
