做跨仓 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 没有开放 fontSize、fontColor、maxLines、textOverflow、lineHeight,业务仓库就改不了这些基础属性。后面每补一个参数,组件库就多一笔接口债。
这不是说 HarmonyOS 不能做跨仓 UI 库。HAR 可以在多个模块或多个工程之间共享 ArkUI 组件、资源和代码。问题在于,ArkUI 的共享边界不应该照搬 Android View 继承模型。

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() 的属性链上。业务要临时加 maxLines、textOverflow、accessibilityText,不用回组件库补接口。
跨仓库还要做什么
跨仓 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,然后把 BrandText、BrandButton、BrandCard、BrandListItem 全塞进去。
刚开始会很统一。过一段时间,问题会集中出现:
| 需求 | 放进大组件库的问题 | 更合适的落点 |
|---|---|---|
| 页面想临时调字号、行数、无障碍描述 | 每个基础属性都要补参数 | 继续用 Text(),叠加 AttributeModifier |
| 不同业务线按钮有 loading、icon、权限态 | BrandButton 参数膨胀 |
做 LoadingButton、PermissionButton 这类结构组件 |
| 深浅色、字号、间距要统一 | 页面各写各的会漂 | 放进 design-core.har 的资源和 token |
| 手机和平板页面结构不同 | 跨仓组件很难知道业务布局 | 应用仓库内做页面模板和视觉回归 |
| 多项目一致性要检查 | 只靠组件库覆盖不到页面组合 | 建 UI review checklist 和截图检查 |
所以更实际的判断是:鸿蒙跨仓组件库要做,但不要把它当作唯一治理入口。
Android 项目里,很多统一性可以靠继承式控件库接住。ArkUI 里,自定义组件一旦包住基础控件,就形成新的边界。跨仓库提供稳定材料和有限结构模式;应用级治理负责把这些材料组合成页面,并用 review、预览、截图检查页面组合是否跑偏。
这条线适合迁移团队记下来:
不要从 BrandText / BrandButton 开始统一所有 UI。
先从 token、资源、AttributeModifier 和高频结构模式开始。
跨仓组件库的作用还在,但它不再适合做“所有 UI 的中心”。在 ArkUI 里,它更像设计材料仓库和模式仓库;页面是否一致、是否可维护,更多要看应用仓库里的 UI 内聚治理。
参考素材(精选)
- Android Developers:Create a custom view class
- Android Developers:Styles and themes
- Android Developers:Implement dark theme
- HarmonyOS:开发静态共享包 HAR
- HarmonyOS:引用及管理共享包
- HarmonyOS:属性修改器 AttributeModifier
- HarmonyOS:@Styles 装饰器
- HarmonyOS:@Extend 装饰器
// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-07-09