How DataList Revolutionized Enterprise WeChat Development on HarmonyOS
Facing the dual challenges of migrating millions of lines of enterprise code to HarmonyOS and maintaining stability amid rapidly evolving APIs, the WeChat team introduced the DataList framework, employing three entropy‑reduction mechanisms—structured, dynamic, and cognitive—to achieve data‑driven UI, cross‑platform reuse, and decoupled business logic.
Author: Huang Wei, Enterprise WeChat client team
When the Enterprise WeChat team started HarmonyOS Next development in 2024, they faced two problems: how to quickly port millions of lines of enterprise applications to a new OS in a small‑team model, and how to keep business code stable while the early HarmonyOS API was still in preview.
The DataList framework provided the answer through three entropy‑reduction mechanisms:
Structured entropy reduction : abstract the process of rendering business logic to UI as a data flow, allowing HarmonyOS and Android to share the same data‑driven development model.
Dynamic entropy reduction : hide HarmonyOS API changes behind an abstract UI data layer, keeping business code unaffected across three major UI revisions.
Cognitive entropy reduction : encapsulate cross‑platform differences into generic components, reducing developers’ mental load and letting them focus on business development.
1. Evolution of the Enterprise WeChat Development Framework
Like building a city, the framework evolved through problem discovery → solution exploration → optimization.
Wild growth (pre‑2019)
Background: lack of unified standards, inconsistent coding styles.
Problem: duplicated implementations and high maintenance cost.
Initial exploration (2019‑2022)
Background: need for a unified development paradigm.
Implementation: EasyList framework with the "everything is a list" concept, encapsulating template code so developers could focus on business logic.
Problem: insufficient isolation between business and UI, resulting in MVC‑like structure and low component reuse.
Gradual improvement (2022‑2024)
Innovation: introduced the DataList framework based on data‑driven, layered isolation.
Value: provided abstraction to lower cognitive burden, enabling every component to be reusable and increasing reusable components from a handful to over 50.
2. Overall Design of the Enterprise WeChat Framework
2.1 Overall Architecture
DataList is a data‑driven, layered isolation framework. The overall architecture diagram is shown below.
We will explain the diagram from the perspective of data flow and layered architecture.
2.2 Data Flow
From the data‑flow perspective, DataList can be divided into Data and List parts:
List: business logic layer, i.e., how business data is transformed into UI data.
Data: data‑driven layer, i.e., how UI data is rendered and how UI refreshes are driven.
2.3 MVDM Circular Layering
DataList isolates the conversion logic from business data to UI data, forming clear boundary layers:
Business Entity Layer (Repo): responsible for requesting and stabilizing business data.
Business Logic Layer (ViewModel): processes business logic and converts business data to UI data.
UI Data Layer (CellData/ViewData): abstracts the UI layer, keeping internal adaptation stable while exposing a stable external interface.
Presentation Layer (Cell): handles actual UI rendering, embracing changes and adapting to new platform features.
This is essentially MVVM transformed into MVDM (Model‑View‑Data‑ViewModel).
Arrows indicate dependency direction.
UI data layer details:
All controls are data‑ized as ViewData.
export class TextData extends BaseData {
text?: string | Resource
fontColor?: ResourceColor
fontSize?: number | string | Resource
fontWeight?: number | FontWeight | string
// ...
}Multiple ViewData objects are combined into a component CellData.
// Component composed of Image + Text
export class ImgTextCellData extends BaseCellData {
builder: WrappedBuilder<[]> = wrapBuilder(ImgTextCellBuilder)
root: RowData
img?: ImgData // corresponds to Image control
text?: TextData // corresponds to Text control
}Because CellData contains no business code, it can be freely reused. Statistics show 58 components with thousands of reuse instances.
Benefits of this layered approach:
Facilitates large‑scale UI reuse
Ensures cross‑platform code consistency
Isolates business from UI changes
2.4 DataList Development Example (No‑trim Required)
“Perfection is achieved not by adding, but by removing.” — Antoine de Saint‑Exupéry
When developing a feature, the indispensable parts are the business‑related sections:
Data request
Business data → UI data conversion
These steps can only be simplified by the framework, not eliminated.
Example: a minimal contact list.
Data request
class DemoContactRepo : IListRepository<DemoContactReq, DemoContactRsp> {
override fun requestData(req: DemoContactReq, callback: (DemoContactRsp) -> Unit, errorCallback: (Int, Any?) -> Unit) {
ContactService.getContact(req) { contacts ->
callback(contacts)
}
}
}Data conversion
class DemoContactViewModel : SingleListViewModel<DemoContactReq, DemoContactRsp>() {
/** Convert business data to UI data */
override fun transferData(data: DemoContactRsp): List<ICellData> {
return data.contacts.map {
ImgPhotoTextImgCellData(
dataId = it.id,
photo = PhotoData(url = it.avatar),
leftText = TextData(text = it.name)
)
}
}
override fun initRepository(): IListRepository<DemoContactReq, DemoContactRsp> = DemoContactRepo()
override fun refreshParam(arguments: Bundle?): DemoContactReq = DemoContactReq(0, 20)
}The total code is 39 lines; the contact list is complete.
If the page is static, network requests can be omitted and only the reusable CellData components are needed, resulting in a complete implementation of about 40 lines.
class DemoAttendanceViewModel : LocalSingleListViewModel() {
// ...
override fun transformCellDataList(): List<ICellData> {
return listOf(
attendanceCellData("打卡人员", "员工A").section(1),
attendanceCellData("规则名称", "打卡规则abc").section(1),
// ... other rows ...
ButtonCellData(ButtonData("删除规则", buttonStyle = R.style.button_l_white, textColor = R.color.day_night_color_chrome_red.getColor())).section(6)
)
}
private fun attendanceCellData(title: String, desc: String): ImgPhotoTextImgCellData {
return ImgPhotoTextImgCellData(/* set properties */)
}
}2.5 MVDM Architecture Delayed‑Decision Practice
“If you want a system that can push work forward, keep as many options open as long as possible.” — The Clean Architecture Way
Through MVDM layering we decouple business logic from UI rendering, but the real test is HarmonyOS Next development where low‑level APIs change like shifting sand. The UI data layer has survived three major architecture revisions while the business layer remained stable.
Compromise version : rapid business start‑up.
Adaptation version : embrace dynamic attribute capabilities.
Optimization version : break performance bottlenecks.
3. First Version – Compromise Under System Constraints
Goal: Rapid Start‑up
All pages were built with DataList, so we needed immediate data‑binding capability.
Idea
In HarmonyOS, UI components are functions, not classes, so we cannot obtain a component instance to assign values directly.
@Component
export struct DemoPage {
build() {
Text("Hello World!") // function, cannot get object for dynamic assignment
}
}Therefore we had to iterate over all properties and invoke them, even if the data does not provide a value.
Solution
List all properties and call each setter; missing values result in null calls.
Problems
Even if only one attribute is set, all functions are executed.
Some attribute functions behave differently when called with null, making them impossible to list.
Ugly and not elegant.
We requested a dynamic attribute solution from Huawei.
4. Second Version – Dynamic Attribute Data Binding
Goal: Integrate Dynamic Attribute Setting
Huawei provided AttributeModifier as the solution.
Solution
@Component
export struct WwText {
@ObjectLink data: TextData
@State modifier: TextModifier = new TextModifier(new TextData())
aboutToAppear(): void {
this.modifier.data = this.data
}
build() {
Text(this.data.text)
.attributeModifier(this.modifier) // update via modifier without extra calls
}
}The update mechanism works as follows:
After @Observed annotation, TextData becomes a dynamic proxy.
Proxy observes property changes.
Recorded setters locate observers.
Observers invoke update functions (complex call chain).
Update function calls applyNormalAttribute in the modifier, finally setting the property on the control.
Compiled TypeScript for WwText:
export class WwText extends ViewPU {
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create(this.data.text);
Text.attributeModifier.bind(this)(ObservedObject.GetRawObject(this.modifier));
}, Text);
Text.pop();
}
}Problems
Two major issues emerged:
1. Code bloat
Each simple line expands to dozens of lines after compilation; a generic component grew from 4 KB to 75 KB.
Code size increased dramatically ("4 KB → 75 KB").
2. Performance loss
Redundant refreshes : even if only one attribute changes, all setters are invoked.
State management overhead : the proxy’s get costs ~9 ms per 10 000 calls; a page with 10 cells × 5 texts × (23 + 45) attributes incurs ~3 ms just for reads, a noticeable fraction of the 8.3 ms frame budget at 120 fps.
Node explosion : wrapping controls adds extra nodes (e.g., Text → WwText adds a node; additional wrappers add more), increasing rendering cost.
5. Third Version – Custom State Management for Performance
Goal: Performance Optimization
The third version addresses the issues of the second version.
Idea
Remove wrapper components and operate directly on native controls, using AttributeUpdater (a subclass of AttributeModifier) to handle attribute updates and constructor changes.
Solution
1. Remove component wrappers
Eliminate the extra code and node overhead; place original logic into corresponding Updaters (e.g., WwText → Text + TextUpdater).
2. Custom state management
Drop the official @Observed annotation on data models and avoid state annotations in Views. Use AttributeUpdater to directly set attributes on native controls, bypassing the proxy‑based state system.
“Only multi‑layer nested listening scenarios need @Observed.”
Since business logic resides in ViewModels, we need a custom mechanism to detect data changes.
Two changes are required:
Remove @Observed from Data.
Remove state annotations from Views.
To drive UI refresh, AttributeUpdater can directly access the attribute object and set properties, while a static proxy replaces the dynamic proxy for better performance.
export class BaseData<INS extends CommonAttribute = CommonAttribute> {
ins?: INS
updateConstructorFunc?: () => void
private _width?: Length
private _height?: Length
set width(width: Length | undefined) {
this._width = width
this.ins?.width(width)
}
get width(): Length | undefined { return this._width }
// ... similar for height and other attributes
} export class BaseUpdater<DATA extends BaseData, T extends CommonAttribute, C = Initializer<T>> extends AttributeUpdater<T, C> {
data?: DATA
constructor(data?: DATA) {
super()
this.data = data
}
updateData(data?: DATA, instance?: T): BaseUpdater<DATA, T, C> {
this.setUpdateFunc(this.data, ins)
if (ins) {
this.applyAttribute(ins, this.data)
this.refreshConstructor()
}
return this
}
applyAttribute(instance: CommonAttribute, data: BaseData) {
if (data.width || data.width == 0) { instance.width(data.width) }
if (data.height || data.height == 0) { instance.height(data.height) }
// ... other attributes
}
}Component implementation now binds CellData to Updaters:
@Component
export struct ImgTextCell {
@Consume @Watch("updateData") cellData: ImgTextCellData
rootUpdater = new RowUpdater()
imgUpdater = new ImageUpdater()
textUpdater = new TextUpdater()
aboutToAppear() { this.updateData() }
aboutToReuse() { this.updateData() }
build() {
Row() {
Image(ImageUpdater.EMPTY).attributeModifier(this.imgUpdater)
Text().attributeModifier(this.textUpdater)
}.attributeModifier(this.rootUpdater)
}
private updateData() {
this.rootUpdater.updateData(this.cellData.root)
this.imgUpdater.updateData(this.cellData.img)
this.textUpdater.updateData(this.cellData.text)
}
}Business usage of CellData and Data remains unchanged.
Results
1. Size reduction
For PhotoTextCell, compiled size dropped to 9.3 % of the original.
2. Performance boost
Reusable PhotoTextCell time reduced from 4.3 ms to 0.9 ms.
Conversation list frame rate improved from a stuttery 32 fps to a smooth 118 fps.
6. Conclusion
In the rapidly changing HarmonyOS ecosystem, DataList’s three‑stage entropy‑reduction strategy establishes a deterministic development paradigm. The three versions illustrate a progressive response to API uncertainty:
Compromise version : quick start via exhaustive property traversal; stable but inefficient.
Adaptation version : dynamic attribute updates via AttributeModifier; functional yet bloated and performance‑heavy.
Optimization version : custom state management and removal of wrappers; achieves 9.3 % code size, 79 % reduction in reuse time, and frame rates jumping from 32 fps to 118 fps.
The MVDM layering consistently isolates UI changes from core business logic, allowing the team to keep the business layer stable while iteratively improving the UI layer.
By the end of 2024, the Enterprise WeChat HarmonyOS team delivered the first HarmonyOS version with over 1 million lines of code and 600+ pages, successfully launching the product.
WeChat Client Technology Team
Official account of the WeChat mobile client development team, sharing development experience, cutting‑edge tech, and little‑known stories across Android, iOS, macOS, Windows Phone, and Windows.
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.
