鸿蒙百篇系列第 77 篇 · 专家
第 50 篇讲过设计模式在鸿蒙里的基础应用。到第 76 篇为止,你已经看过分层架构、错误体系、性能治理、热修复、跨平台工程等专家级实践。这篇把它们串起来——站在架构层面,系统梳理 ArkTS/ArkUI 中设计模式的选型与落地。
设计模式不是背出来的,是被代码量逼出来的。当你的应用超过 30 个页面、模块间依赖错综复杂时,选对模式和选错模式的差距,就是"改一晚上能上线"和"改一星期还在填坑"的差距。
一、单例模式:AppStorage vs 手写单例
单例在鸿蒙里是最容易被滥用的模式。很多开发者从 Android 迁移过来,习惯写一个 getInstance() 的全局管理类。但 ArkUI 框架本身已经提供了单例基础设施。
框架内置的单例
第 3 篇和第 58 篇讲过 AppStorage 的基本用法。这里要说的是:什么情况下你仍然需要手写单例。
什么时候需要手写单例
AppStorage 管的是 UI 状态。如果你的对象不参与 UI 渲染,只做业务逻辑(比如网络层、数据库管理器、日志管理器),用 AppStorage 就不合适——它会把数据绑定到 UI 框架上,引发不必要的渲染开销。
// 业务层单例:不依赖 UI 框架
export class NetworkManager {
private static instance: NetworkManager | null = null
private baseURL: string = ''
private interceptors: RequestInterceptor[] = []
private constructor() {}
static getInstance(): NetworkManager {
if (NetworkManager.instance === null) {
NetworkManager.instance = new NetworkManager()
}
return NetworkManager.instance
}
setBaseURL(url: string): void {
this.baseURL = url
}
addInterceptor(interceptor: RequestInterceptor): void {
this.interceptors.push(interceptor)
}
async request(config: RequestConfig): Promise<Response> {
// 拦截器链处理
for (const interceptor of this.interceptors) {
config = await interceptor.onRequest(config)
}
// 发起请求...
return {} as Response
}
}
选型决策
二、观察者模式:状态装饰器的本质
ArkUI 的 @State、@Link、@Watch 本质上就是观察者模式的框架级实现。理解这一点,才能在复杂场景下做出正确的状态管理选型。
V1 vs V2 状态管理
第 3 篇讲过 V1 的基本装饰器。API 12 之后,ArkUI 推出了状态管理 V2,核心差异在观察粒度:
// V1:代理观察,只能感知第一层
@Observed
class UserInfo {
name: string = ''
age: number = 0
address: Address = new Address() // 嵌套对象的属性变化无法感知
}
// V2:数据本身可观察,支持深度观测
@ObservedV2
class UserInfoV2 {
@Trace name: string = ''
@Trace age: number = 0
@Trace address: AddressV2 = new AddressV2()
}
@ObservedV2
class AddressV2 {
@Trace city: string = ''
@Trace street: string = ''
}
// 只有 city 变化时,只更新引用 city 的 Text 组件
@ComponentV2
struct UserCard {
@Param user: UserInfoV2 = new UserInfoV2()
build() {
Column() {
Text(this.user.name)
Text(this.user.address.city) // 只在 city 变化时重渲染
}
}
}
@Watch 的正确用法
@Watch 是观察者模式的轻量补充,但它不是万能的:
@Entry
@Component
struct OrderPage {
@State @Watch('onPriceChange') price: number = 0
@State @Watch('onQtyChange') quantity: number = 1
@State total: number = 0
onPriceChange() {
// 注意:这里不要做重计算,只做派生状态更新
this.total = this.price * this.quantity
}
onQtyChange() {
this.total = this.price * this.quantity
}
build() {
// ...
}
}
@Watch 回调在主线程同步执行。如果你在里面做网络请求或文件 IO,会卡帧。它只适合做轻量的派生计算。
三、策略模式:条件渲染的工程化
策略模式在 ArkUI 中的应用场景比你想的多。最典型的是:同一个位置根据不同条件渲染完全不同的内容。
反模式:堆 if/else
// 坏例子:随着业务增长,这个 build 会越来越长
@Component
struct ContentArea {
@State contentType: string = 'article'
build() {
Column() {
if (this.contentType === 'article') {
// 50 行文章渲染逻辑
} else if (this.contentType === 'video') {
// 50 行视频渲染逻辑
} else if (this.contentType === 'live') {
// 50 行直播渲染逻辑
} else if (this.contentType === 'gallery') {
// 50 行图集渲染逻辑
}
}
}
}
策略模式:Builder 注册
// 定义渲染策略接口
interface ContentStrategy {
build(content: ContentData): void
}
@Builder
function ArticleBuilder(content: ContentData) {
Column() {
Text(content.title).fontSize(20)
Text(content.body).fontSize(16)
}
}
@Builder
function VideoBuilder(content: ContentData) {
Column() {
Video({ src: content.videoUrl })
Text(content.title)
}
}
@Builder
function LiveBuilder(content: ContentData) {
Column() {
Text('直播中: ' + content.title)
// 直播组件
}
}
// 策略注册表
const contentStrategies: Record<string, (content: ContentData) => void> = {
'article': ArticleBuilder,
'video': VideoBuilder,
'live': LiveBuilder
}
// 使用方:一行调度
@Component
struct ContentArea {
@Prop contentType: string = 'article'
@Prop contentData: ContentData = new ContentData()
@Builder
renderContent() {
contentStrategies[this.contentType]?.(this.contentData)
}
build() {
Column() {
this.renderContent()
}
}
}
新增内容类型时,只需要注册一个新 Builder,不用改 ContentArea 组件。第 54 篇的表单引擎设计也用了类似的思路。
四、MVI:响应式架构在鸿蒙的落地
MVI(Model-View-Intent)是响应式架构在 UI 层的落地模式。和 MVC/MVP 相比,它的核心是单向数据流。
数据流
用户操作 → Intent → Model(状态变更) → View(UI 刷新)
↑ │
└──────────── 事件驱动 ←───────────────────┘
代码骨架
// 定义状态
interface CounterState {
count: number
isLoading: boolean
error: string | null
}
// 定义意图
type CounterIntent =
| { action: 'increment' }
| { action: 'decrement' }
| { action: 'reset' }
| { action: 'loadFromServer' }
// 状态容器
class CounterStore {
@State state: CounterState = {
count: 0,
isLoading: false,
error: null
}
dispatch(intent: CounterIntent): void {
switch (intent.action) {
case 'increment':
this.state.count++
break
case 'decrement':
this.state.count--
break
case 'reset':
this.state.count = 0
this.state.error = null
break
case 'loadFromServer':
this.state.isLoading = true
this.loadFromServer()
break
}
}
private async loadFromServer(): Promise<void> {
try {
const result = await fetchCount()
this.state.count = result
this.state.isLoading = false
} catch (e) {
this.state.error = (e as Error).message
this.state.isLoading = false
}
}
}
// View 只负责渲染状态和派发意图
@Entry
@Component
struct CounterPage {
private store: CounterStore = new CounterStore()
build() {
Column() {
if (this.store.state.isLoading) {
LoadingProgress()
} else {
Text(`Count: ${this.store.state.count}`)
}
if (this.store.state.error !== null) {
Text(this.store.state.error).fontColor(Color.Red)
}
Button('+').onClick(() => this.store.dispatch({ action: 'increment' }))
Button('-').onClick(() => this.store.dispatch({ action: 'decrement' }))
Button('Reset').onClick(() => this.store.dispatch({ action: 'reset' }))
}
}
}
MVI vs MVC vs MVP
第 39 篇讲过架构演进路线。MVI 是那条路线的终点——大型鸿蒙应用推荐用 MVI 管理核心业务流,简单页面用 @State + @Link 足够。
五、工厂模式:组件动态创建
第 34 篇讲组件化时提过工厂模式。这里补充一个实际场景:根据配置动态创建不同类型的卡片。
interface CardConfig {
type: CardType
data: CardData
}
enum CardType {
BANNER = 0,
LIST = 1,
GRID = 2,
VIDEO = 3
}
class CardFactory {
@Builder
static createCard(config: CardConfig): void {
switch (config.type) {
case CardType.BANNER:
BannerCard({ data: config.data })
break
case CardType.LIST:
ListCard({ data: config.data })
break
case CardType.GRID:
GridCard({ data: config.data })
break
case CardType.VIDEO:
VideoCard({ data: config.data })
break
}
}
}
// 服务端下发卡片配置列表,端侧动态渲染
@Component
struct DynamicFeedPage {
@State cardConfigs: CardConfig[] = []
build() {
List() {
LazyForEach(this.dataSource, (config: CardConfig) => {
ListItem() {
CardFactory.createCard(config)
}
}, (config: CardConfig) => config.data.id)
}
}
}
这种模式在信息流、动态首页等场景很常见。服务端控制内容编排,端侧只管渲染。
六、适配器模式:NAPI 桥接
第 43 篇和第 64 篇讲过 NAPI 原生模块开发。适配器模式在这里的作用是:隔离 ArkTS 层和 C++ 层的数据结构差异。
// C++ 层返回的数据结构
interface NativeDeviceResult {
deviceId: string
rawName: Uint8Array // 原始字节,需要解码
status: number
extras: Map<string, string>
}
// ArkTS 层使用的领域模型
class DeviceModel {
id: string
displayName: string
isActive: boolean
extraInfo: Record<string, string>
constructor(id: string, name: string, active: boolean, extras: Record<string, string>) {
this.id = id
this.displayName = name
this.isActive = active
this.extraInfo = extras
}
}
// 适配器:转换 + 解码
class DeviceAdapter {
static toModel(raw: NativeDeviceResult): DeviceModel {
return new DeviceModel(
raw.deviceId,
this.decodeUTF8(raw.rawName),
raw.status === 1,
this.mapToObject(raw.extras)
)
}
private static decodeUTF8(bytes: Uint8Array): string {
const decoder = new TextDecoder('utf-8')
return decoder.decode(bytes)
}
private static mapToObject(map: Map<string, string>): Record<string, string> {
const obj: Record<string, string> = {}
map.forEach((value, key) => {
obj[key] = value
})
return obj
}
}
适配器的价值在于:当 C++ 层的数据结构变化时,只需要改 Adapter,ArkTS 业务层完全不受影响。
七、模式选型速查表
小结
设计模式在鸿蒙开发中的落地,和教科书上的理论有很大不同。框架已经内置了单例(AppStorage)、观察者(状态装饰器)、建造者(链式配置)等模式的实现,你的工作是在此基础上补齐策略、工厂、适配器、MVI 这些工程级模式。
选择模式的核心标准只有一个:它是否降低了你的代码复杂度。如果一个模式引入的抽象层比它解决的问题还复杂,那就是过度设计。