跪拜 Guibai
← Back to the summary

HarmonyOS Componentization Without Reflection: Contracts, Registries, and Unidirectional Dependencies

Based on the hmsmoduledemo sample project (login / registration / password change / forced game update / payment), explain the componentization implementation method of 'package type isolation + interface contract + unidirectional dependency'.

Screenshots

image.png

image.png

image.png

image.png

image.png

1. Why Componentization

In a monolithic project, all code is mixed in one module. As features grow, problems appear:

The core idea of componentization: split packages by business boundaries, modules communicate only through 'contracts', no one knows anyone else's implementation.

Three iron rules of this project:

  1. Package Type Isolation — Different responsibilities use different package types (HAP / HAR / HSP);
  2. Interface Contract — Cross-module calls only depend on interfaces in common_api, not on implementation classes;
  3. Unidirectional Dependency — Upper layers can depend on lower layers, lower layers absolutely never depend on upper layers, business modules do not import each other.

2. Three Package Types: HAP / HAR / HSP

Package Type Full Name Characteristics Role in This Project
HAP HarmonyOS Ability Package App installation/runtime unit, can contain Abilities, finally packaged into the app entry: Shell project
HAR HarmonyOS Archive Static shared package, each HAP that references it gets its own copy, code is copied with the package common / common_api / user_module / pay_module / update_module
HSP HarmonyOS Shared Package Dynamic shared package, only one copy exists within the same device and app, shared at runtime common_service: Global login state

Key memory points for differentiation:


3. Overall Architecture: Unidirectional Dependency Layering

entry (Entry HAP)                       ← Shell project: Navigation assembly + module initialization
├── user_module   (HAR)  Login / Registration / Change Password
├── pay_module    (HAR)  Payment Center (Product → Order → Pay)
├── update_module (HAR)  Forced Update (Version check + mandatory update dialog)
├── common_service(HSP)  SessionManager: Login state AppStorage wrapper (global singleton)
├── common_api    (HAR)  Contract layer: IUserService / IPayService / IUpdateService
│                        ServiceRegistry / RouteCenter / Models / Event constants
└── common        (HAR)  Basic capabilities: Logger / Validators / MockHttp

Dependency relationships are reflected in each module's oh-package.json5 under file: local dependencies, for example entry/oh-package.json5:

"dependencies": {
  "@demo/common": "file:../common",
  "@demo/common_api": "file:../common_api",
  "@demo/common_service": "file:../common_service",
  "@demo/user_module": "file:../user_module",
  "@demo/update_module": "file:../update_module",
  "@demo/pay_module": "file:../pay_module"
}

4. Four Inter-Module Communication Mechanisms (Key Points)

ArkTS does not support reflection (no Class.forName), so 'interface finding implementation' must be done explicitly. This project provides four complementary mechanisms:

4.1 Request-Response: ServiceRegistry (Interface Registry)

Location: common_api/src/main/ets/registry/ServiceRegistry.ets

export class ServiceRegistry {
  private static services: Map<string, Object> = new Map<string, Object>();

  static register<T extends Object>(key: string, impl: T): void { ... }
  static get<T extends Object>(key: string): T | undefined { ... }
}

Workflow (using payment as an example):

  1. Contract defined in common_api: IPayService interface declares queryProducts / createOrder / pay;
  2. Implementer registers: Inside PayModule.init(), ServiceRegistry.register<IPayService>(ServiceKeys.PAY, new PayServiceImpl());
  3. Consumer gets: Any page ServiceRegistry.get<IPayService>(ServiceKeys.PAY)?.createOrder(...).

Key points:

4.2 Page Navigation: RouteCenter + Navigation Module Self-Registration

Location: common_api/src/main/ets/registry/RouteCenter.ets

HarmonyOS page routing uses the Navigation + NavDestination system, but the route table is centralized in entry, while business pages are in various HARs. entry should not directly import business pages (otherwise the shell and business are coupled again). The solution:

  1. Each business module declares a page builder function using @Builder, and during module initialization, wraps it into a WrappedBuilder via the global function wrapBuilder and registers it with RouteCenter:
// user_module/UserModule.ets
@Builder
function LoginPageBuilder(params: RouteParams): void {
  LoginView({ params });
}

RouteCenter.register({ name: RouteNames.LOGIN, builder: wrapBuilder(LoginPageBuilder) });
  1. entry's navDestination uniformly retrieves the builder from RouteCenter by name:
// entry/pages/Index.ets
@Builder
pageMap(name: string, param: ESObject): void {
  NavDestination() {
    if (RouteCenter.has(name)) {
      RouteCenter.get(name)?.builder.builder(param as RouteParams)
    } else {
      Text(`Page not registered: ${name}`)
    }
  }
  ...
}
  1. Navigation becomes purely data-driven: RouteCenter.push(pathStack, RouteNames.PAY_CENTER).

Key points:

4.3 Global State: AppStorage + HSP Singleton

Location: common_service/src/main/ets/session/SessionManager.ets

export class SessionManager {
  static onLogin(user: UserInfo): void {
    AppStorage.setOrCreate('isLogin', true);
    AppStorage.setOrCreate('KEY_USER_INFO', user);
    ...
  }
}
@StorageLink('isLogin') isLogin: boolean = false;

4.4 Notification Events: emitter Event Bus

Location: common_api/src/main/ets/constants/Events.ets

// Send (after UserServiceImpl login succeeds)
emitter.emit({ eventId: EVENT_USER_LOGIN }, { data: { userId } });

Usage principles (discipline written in project comments):

How to Choose Among the Four Mechanisms

Requirement Use
Call someone else's function and get a result ServiceRegistry
Navigate to a page RouteCenter
Multiple UIs share a state and need automatic refresh AppStorage (wrapped in HSP)
Broadcast a notification after completing an action emitter

5. Key Business Flow Walkthroughs

5.1 Startup Initialization

EntryAbility.onCreate (entry/src/main/ets/entryability/EntryAbility.ets):

UserModule.init();
UpdateModule.init();
PayModule.init();

Each init() does two things: registers the service implementation with ServiceRegistry, registers the page builder with RouteCenter. Explicit registration replaces reflection, traceable at compile time. If a module isn't initialized, you'll know immediately at runtime (service get returns undefined, page displays 'not registered').

5.2 Login Flow

LoginViewIUserService.login()UserServiceImpl validates account via MockHttp → On success:

  1. SessionManager.onLogin(user) writes to AppStorage;
  2. emitter.emit(EVENT_USER_LOGIN) broadcasts;
  3. Home page @StorageLink('isLogin') automatically refreshes to show 'Hello, Test Player'.

Test account: 13800000000 / abc123.

5.3 Forced Update Flow

Home page aboutToAppearbundleManager.getBundleInfoForSelf reads local versionCode (AppScope configured as 100) → IUpdateService.checkUpdate() compares against Mock server minSupportCode=150100 < 150 triggers mandatory dialog ForceUpdateDialog (autoCancel: false, only 'Update Now' or 'Exit Game').

Note the forced update dialog does not go through routing: it's a component, not a page, directly exported by update_module for entry to use — component-level reuse can be directly exported, page-level navigation requires RouteCenter.

5.4 Payment Flow

PayCenterView loads product list → selects channel (Huawei/WeChat/Alipay placeholder) → createOrderpay:


6. Design Intent of the Mock Network Layer

common/src/main/ets/network/MockHttp.ets uses setTimeout(600ms) + in-memory data to simulate the network:

static request(api: string, params: ESObject, handler: (p: ESObject) => MockResponse): Promise<MockResponse>

All business code is written in the form of 'async request → get response → handle code'. In a real project, just replace MockHttp's implementation with @kit.NetworkKit's http.createHttp().request(), and the business modules do not need a single line changed — this is the replacement freedom brought by contracts/layering.


7. Pitfalls Encountered (Errors Actually Fixed in This Project)

These errors are very typical in componentized HarmonyOS projects and worth remembering:

  1. HAR module missing src/main/module.json5 → Build error module.json5 file not found. Every module must have one, type filled with har / shared / entry.
  2. AppScope/app.json5 missing icon / label → Schema validation fails. Also, icon resources must be placed under AppScope/resources/, cannot reference entry's resources.
  3. Key name in main_pages.json: API 12 requires "src": [...], not "pages": [...].
  4. Do not write @ohos/hvigor dependency in root oh-package.json5: hvigor plugin is managed by hvigor/hvigor-config.json5, writing it in oh-package causes ohpm install to fetch from ohpm registry and report 404.
  5. 0xDEMO is not a valid hexadecimal (M is not a hex char) → Compilation throws a strange binary expression error.
  6. WrappedBuilder / NavPathStack / wrapBuilder are global symbols, importing from @kit.ArkUI instead reports "has no exported member".
  7. ArkTS does not allow object literals as types: { key: PayChannel; label: string }[] must be extracted into an interface.
  8. Cannot write regular statements inside UI build scopes: Declarations like const route = ... are not allowed inside @Builder / build(), change to if/else + component calls.
  9. System color resource names must align with SDK: e.g., sys.color.ohos_id_color_sub_emphasize does not exist in the current SDK, check the SDK directory toolchains/id_defined.json to confirm available names.
  10. Fix order: First fix package structure/resources (PreBuild), then install dependencies (ohpm install), finally ArkTS compilation errors — preceding errors will mask the real errors behind them.

8. Hands-on Exercise Suggestions

  1. Add a 'Settings Module' (setting_module HAR): Provide ISettingService contract (read/save volume switch), add an entry on the entry home page. Go through the full process of 'define contract → implement registration → page self-registration → shell assembly'.
  2. Change the product list to refresh via emitter notification: Simulate 'product inventory changes after successful payment', experience the division of labor between event bus and ServiceRegistry.
  3. Replace MockHttp with real http requests: Verify if business modules truly require zero changes.
  4. Intentionally let user_module import pay_module's implementation class, observe what happens to compilation/architecture when dependency rules are broken, then change back to contract calls.

9. Reference File Quick Lookup

Content Path
Service Registry common_api/src/main/ets/registry/ServiceRegistry.ets
Route Center common_api/src/main/ets/registry/RouteCenter.ets
Login State Wrapper (HSP) common_service/src/main/ets/session/SessionManager.ets
Event Constants common_api/src/main/ets/constants/Events.ets
Route Name Constants common_api/src/main/ets/constants/RouteNames.ets
Shared Data Models common_api/src/main/ets/model/Models.ets
Mock Network Layer common/src/main/ets/network/MockHttp.ets
Module Initialization Entry entry/src/main/ets/entryability/EntryAbility.ets
Navigation Assembly entry/src/main/ets/pages/Index.ets

Project Address

https://gitee.com/qiuyu123/hmscomponentization

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

沉默王贰

I've also studied HarmonyOS componentization before, following the official recommendations, which is relatively simple.

xq9527

This lacks the ability to freely switch between application and library like on Android, so it still falls a bit short.