跨仓组件库在 ArkUI 里还成立吗:从自定义控件到应用级 UI 治理

做跨仓 UI 库时,Android 开发者很容易从一个增强控件开始。

比如做一个 BrandTextView

class BrandTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.brandTextViewStyle
) : AppCompatTextView(context, attrs, defStyleAttr) {

    fun setBrandLevel(level: BrandTextLevel) {
        // 设置品牌字号、字重、颜色、行高等规则
    }
}

业务仓库里使用它时,调用方仍然把它当 TextView 看:

<com.example.design.BrandTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="订单标题"
    android:textSize="18sp"
    android:textColor="?attr/colorOnSurface"
    android:maxLines="1"
    android:ellipsize="end" />

这就是 Android 跨仓组件库好用的地方:组件库新增品牌能力,但没有截断父控件能力。TextView 原来的 XML 属性、style、theme attr、values-night 资源选择还在,业务仓库可以继续用自己熟悉的方式做局部调整。

迁到 ArkUI 后,同样写一个 BrandText,边界马上变了:

@Component
export struct BrandText {
  text: string = ''

  build() {
    Text(this.text)
      .fontSize(16)
      .fontColor($r('app.color.text_primary'))
      .fontWeight(FontWeight.Medium)
  }
}

页面里拿到的是 BrandText,不是内部那颗 Text

BrandText({ text: '订单标题' })
  .fontSize(18) // 这里不是 Text 的 fontSize 调用面

如果 BrandText 没有开放 fontSizefontColormaxLinestextOverflowlineHeight,业务仓库就改不了这些基础属性。后面每补一个参数,组件库就多一笔接口债。

这不是说 HarmonyOS 不能做跨仓 UI 库。HAR 可以在多个模块或多个工程之间共享 ArkUI 组件、资源和代码。问题在于,ArkUI 的共享边界不应该照搬 Android View 继承模型。

Android 组件库到 ArkUI UI 治理的边界变化

Android 的跨仓控件,靠继承保住开放面

Android 里一个增强控件能跨仓复用,通常不是因为它封装得足够厚,而是因为它足够薄。

BrandTextView 继承 TextView,所以调用方继续拥有 TextView 的属性面。组件库只补品牌层能力,比如默认字号、默认颜色、业务状态、埋点、无障碍规则。

style 和 theme 又把这件事往前推了一步:

<!-- res/values/attrs.xml -->
<attr name="brandTextViewStyle" format="reference" />
<!-- res/values/themes.xml -->
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="brandTextViewStyle">@style/Widget.App.BrandTextView</item>
</style>
<!-- res/values/styles.xml -->
<style name="Widget.App.BrandTextView">
    <item name="android:textSize">16sp</item>
    <item name="android:textColor">?attr/colorOnSurface</item>
    <item name="android:includeFontPadding">false</item>
</style>

再加一个暗色资源:

<!-- res/values-night/colors.xml -->
<color name="text_primary">#F8FAFC</color>

跨仓组件库只需要定义一个入口,业务仓库还可以通过 theme、style、XML 属性继续接住变化。Android 的优势不是“组件库包得多”,而是基础控件、资源系统、style 链和自定义 View 继承在同一套模型里工作。

ArkUI 也能共享,但不要把叶子控件包死

HarmonyOS 的共享能力是存在的。HAR 可以放 ArkUI 组件、资源、工具代码,再被其他模块或工程引用。对多项目团队来说,design-core.har 当然有意义。

叶子控件封装要少做。

下面这种写法短期看着统一:

BrandButton({
  text: '提交',
  type: 'primary',
  size: 'small'
})

但它很快会遇到这些需求:

// 业务仓库可能想改这些
.width('100%')
.fontSize(15)
.constraintSize({ minHeight: 44 })
.accessibilityText('提交订单')
.stateStyles({
  pressed: { opacity: 0.8 }
})

如果 BrandButton 没有提前开放这些参数,调用方就只能改组件库。Android 里的 BrandButton extends AppCompatButton 不太怕这个问题,因为它天然保留了 Button / TextView 的开放面。ArkUI 组合组件没有自动继承内部组件的属性链。

@Styles@Extend 也不能直接补上这个缺口。官方文档里,@Styles 只能在当前文件使用,不支持 export@Extend 可以扩展指定组件样式,也支持参数,但同样不支持导出。跨文件复用样式逻辑时,官方更推荐 AttributeModifier

所以 ArkUI 跨仓 UI 库的基础层,应该优先考虑这种写法:

export type BrandTextTone = 'primary' | 'secondary' | 'danger'
export type BrandTextSize = 'body' | 'title' | 'caption'

export interface BrandTextOptions {
  tone?: BrandTextTone
  size?: BrandTextSize
}

export class BrandTextModifier implements AttributeModifier<TextAttribute> {
  constructor(private options: BrandTextOptions = {}) {}

  applyNormalAttribute(instance: TextAttribute): void {
    const tone = this.options.tone ?? 'primary'
    const size = this.options.size ?? 'body'

    instance
      .fontColor(textColorOf(tone))
      .fontSize(textSizeOf(size))
      .fontWeight(size === 'title' ? FontWeight.Medium : FontWeight.Regular)
  }
}

页面里继续直接使用 Text

Text('订单标题')
  .attributeModifier(new BrandTextModifier({
    tone: 'primary',
    size: 'title'
  }))
  .maxLines(1)
  .textOverflow({ overflow: TextOverflow.Ellipsis })

这个写法的好处是:品牌样式可以跨仓复用,但调用方仍然站在 Text() 的属性链上。业务要临时加 maxLinestextOverflowaccessibilityText,不用回组件库补接口。

跨仓库还要做什么

跨仓 UI 库不是消失了,只是职责要收缩。

第一层是 design-core.har。它放稳定材料:

design-core.har
  resources/base/element/color.json
  resources/dark/element/color.json
  tokens/spacing.ets
  tokens/typography.ets
  modifiers/BrandTextModifier.ets
  modifiers/BrandButtonModifier.ets

这一层不应该关心订单页、会员页、活动页长什么样。它只回答:品牌色怎么取、字号阶梯怎么定义、间距刻度怎么命名、基础控件常用 modifier 怎么写。

第二层是 ui-patterns.har。它放少量稳定结构组件:

ui-patterns.har
  EmptyState.ets
  LoadingButton.ets
  PermissionBanner.ets
  InfoCard.ets

这些组件不是 Text / Button 的替身,而是结构已经稳定的业务模式。比如 EmptyState 就是图标、标题、说明、主按钮、次按钮;LoadingButton 就是按钮文本、loading 状态、禁用状态和点击回调。它们的 API 应该窄一点,不要试图暴露所有底层属性。

第三层放在应用仓库里:

app-ui/
  pages/
  feature-components/
  screen-templates/
  visual-regression/
  ui-review-checklist.md

页面模板、业务组件、深浅色实际效果、平板/手机结构切换,很多时候更适合在应用仓库内治理。因为这些变化跟具体业务强绑定,跨仓组件库很难提前设计出所有参数。

应用级 UI 内聚治理更像主战场

如果团队有多个 HarmonyOS 应用,最容易走偏的做法是建一个很大的 ui-kit.har,然后把 BrandTextBrandButtonBrandCardBrandListItem 全塞进去。

刚开始会很统一。过一段时间,问题会集中出现:

需求 放进大组件库的问题 更合适的落点
页面想临时调字号、行数、无障碍描述 每个基础属性都要补参数 继续用 Text(),叠加 AttributeModifier
不同业务线按钮有 loading、icon、权限态 BrandButton 参数膨胀 LoadingButtonPermissionButton 这类结构组件
深浅色、字号、间距要统一 页面各写各的会漂 放进 design-core.har 的资源和 token
手机和平板页面结构不同 跨仓组件很难知道业务布局 应用仓库内做页面模板和视觉回归
多项目一致性要检查 只靠组件库覆盖不到页面组合 建 UI review checklist 和截图检查

所以更实际的判断是:鸿蒙跨仓组件库要做,但不要把它当作唯一治理入口。

Android 项目里,很多统一性可以靠继承式控件库接住。ArkUI 里,自定义组件一旦包住基础控件,就形成新的边界。跨仓库提供稳定材料和有限结构模式;应用级治理负责把这些材料组合成页面,并用 review、预览、截图检查页面组合是否跑偏。

这条线适合迁移团队记下来:

不要从 BrandText / BrandButton 开始统一所有 UI。
先从 token、资源、AttributeModifier 和高频结构模式开始。

跨仓组件库的作用还在,但它不再适合做“所有 UI 的中心”。在 ArkUI 里,它更像设计材料仓库和模式仓库;页面是否一致、是否可维护,更多要看应用仓库里的 UI 内聚治理。

参考素材(精选)

// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-07-09

上一篇