HarmonyOS开发中ArkTS @Extend装饰器:给组件“装外挂”,让样式复用不再将就

ArkTS @Extend装饰器:给组件"装外挂",让样式复用不再将就


当@Styles遇到"专属属性"的尴尬

记得去年做项目评审,团队里的小王展示他的代码,一脸得意:"看,我用@Styles把按钮样式封装得多漂亮!"我凑过去一看,确实挺整洁------直到我注意到这个细节:

typescript 复制代码
@Styles function buttonStyle() {
  .width(100)
  .height(44)
  .backgroundColor('#007AFF')
  .fontColor(Color.White)  //  等等,Button有fontColor属性吗?
}

Button('提交').buttonStyle()  // 编译通过,但运行时可能出问题

小王愣住了:"啊?Button不是应该有fontColor吗?"我摇摇头:"那是Text的属性,Button用的是font。"

这就是@Styles的局限------它只能处理所有组件都有的通用属性 。一旦遇到某个组件特有的属性,比如Text的fontColor、Button的type、Image的objectFit,@Styles就束手无策了。

直到@Extend出现,这个问题才有了优雅的解决方案。它不像@Styles那样"一视同仁",而是为特定组件量身定制扩展方法。用个不太恰当的比喻:@Styles是给所有家具刷同一种漆,@Extend是给椅子加靠垫、给桌子加抽屉------各取所需。


二、@Extend在编译时到底干了什么?

1. 编译期"魔法"的全过程
运行时真相
开发者视角
编译转换
源代码: @Extend(Text) function titleStyle()
编译器解析
识别 @Extend 装饰器
提取目标组件类型: Text
生成原型链扩展代码
将方法挂载到 Text 原型上
编译后: Text.prototype.titleStyle = function() {...}
Text('标题').titleStyle()
Text('标题').fontSize(18).fontWeight('bold')...

看到这个流程图了吗?@Extend的本质是在编译时为特定组件类型添加原型方法 。编译器看到@Extend(Text),就会在Text的原型链上挂一个titleStyle方法。当你调用Text('标题').titleStyle()时,实际上是在调用这个扩展方法。

这解释了为什么@Extend必须全局定义------原型方法得在组件创建前就挂上去啊。也解释了为什么它能访问组件的私有属性:方法本来就是挂在这个组件原型上的,自然能访问所有属性和方法。

2. 与@Styles的本质区别

很多人把@Extend和@Styles搞混,其实记住一个核心区别就行:

  • @Styles :编译时展开替换。像宏定义,调用处直接替换成样式代码。
  • @Extend :编译时原型扩展。给组件类型添加新方法,运行时调用。
typescript 复制代码
// @Styles:编译时展开
@Styles function cardStyle() {
  .width(200).height(100)  // 编译后直接替换
}

// 编译前
Column().cardStyle()

// 编译后
Column().width(200).height(100)

// @Extend:运行时调用
@Extend(Text) function fancyText() {
  .fontColor(Color.Red)  // 编译后成为Text的原型方法
}

// 编译前
Text('Hello').fancyText()

// 编译后(伪代码)
Text('Hello').prototype.fancyText()

三、核心特性:@Extend的"三板斧"

1. 支持参数------这才是真正的"动态样式"

@Styles最大的痛点是没法传参数,所有样式都得写死。@Extend解决了这个问题:

typescript 复制代码
// 基础版:尺寸参数
@Extend(Text) function responsiveText(size: number) {
  .fontSize(size)
  .lineHeight(size * 1.5)  // 行高随字体大小变化
}

// 使用
Text('小标题').responsiveText(14)
Text('大标题').responsiveText(24)

// 进阶版:完整配置
@Extend(Button) function configurableButton(
  text: string,
  type: ButtonType = ButtonType.Capsule,
  backgroundColor: Color = Color.Blue,
  textColor: Color = Color.White
) {
  .width(120)
  .height(44)
  .type(type)
  .backgroundColor(backgroundColor)
  .fontColor(textColor)
  .fontSize(16)
  .borderRadius(22)
}

// 像调用函数一样使用
Button().configurableButton('提交', ButtonType.Capsule, Color.Green, Color.White)

2. 支持组件私有属性和事件

这是@Extend的杀手锏------能访问组件的"私房"功能:

typescript 复制代码
// Text的私有属性:fontColor、textAlign、decoration...
@Extend(Text) function linkText(url: string) {
  .fontColor('#007AFF')
  .decoration({ type: TextDecorationType.Underline })
  .onClick(() => {
    // 点击跳转
    router.pushUrl({ url: url })
  })
}

// Button的私有属性:type、stateEffect...
@Extend(Button) function capsuleButton(text: string) {
  .type(ButtonType.Capsule)  // @Styles不能用这个!
  .stateEffect(true)         //  @Styles也不能用!
  .width(100)
  .height(44)
}

// Image的私有属性:objectFit、interpolation...
@Extend(Image) function avatarImage(size: number) {
  .width(size)
  .height(size)
  .borderRadius(size / 2)    // 圆形头像
  .objectFit(ImageFit.Cover) // @Styles的禁区!
  .interpolation(ImageInterpolation.High) // 高质量缩放
}

3. 方法嵌套调用------像搭积木一样构建样式

@Extend支持方法调用方法,实现样式的模块化组合:

typescript 复制代码
// 基础积木
@Extend(Text) function baseText() {
  .fontFamily('HarmonyOS Sans')
  .fontColor('#1C1C1E')
}

@Extend(Text) function spacing() {
  .margin({ top: 8, bottom: 8 })
  .padding({ left: 12, right: 12 })
}

// 组合积木
@Extend(Text) function titleText(size: number) {
  .fontSize(size)
  .fontWeight(FontWeight.Bold)
  .baseText()      // 调用其他@Extend方法
  .spacing()       // 再调用一个
}

@Extend(Text) function bodyText() {
  .fontSize(14)
  .lineHeight(20)
  .baseText()
  .spacing()
}

// 使用:清晰得像读文章
Text('文章标题').titleText(18)
Text('正文内容...').bodyText()

四、实战对比:电商应用的按钮系统

让我们看一个真实案例,感受@Extend的威力。

1. 问题:混乱的按钮样式管理

typescript 复制代码
// 改造前:到处都是重复且不一致的按钮代码
@Component
struct ProductDetailOld {
  build() {
    Column() {
      // 主要按钮 - 写法1
      Button('立即购买')
        .width(120)
        .height(44)
        .type(ButtonType.Capsule)
        .backgroundColor('#FF6B35')
        .fontColor(Color.White)
        .fontSize(16)
      
      // 次要按钮 - 写法2(居然不一样!)
      Button('加入购物车')
        .width(110)  // 宽度不同
        .height(40)  //  高度不同
        .type(ButtonType.Normal)  //  类型不同
        .backgroundColor(Color.White)
        .fontColor('#FF6B35')
        .fontSize(14)  // 字体大小不同
        .border({ width: 1, color: '#FF6B35' })
      
      // 禁用按钮 - 写法3(又不一样!)
      Button('已售罄')
        .width(120)
        .height(44)
        .type(ButtonType.Capsule)
        .backgroundColor('#E5E5EA')
        .fontColor('#8E8E93')
        .fontSize(16)
        .enabled(false)  // 禁用状态
    }
  }
}

三个按钮,三种写法,三个维护点。产品经理说"把圆角调大点"?准备改三个地方吧。

2. @Extend解决方案:一套统一的按钮系统

typescript 复制代码
// 按钮设计系统 - 定义一次,到处使用
@Extend(Button) function buttonBase() {
  .width(120)
  .height(44)
  .type(ButtonType.Capsule)
  .fontSize(16)
  .borderRadius(22)
}

@Extend(Button) function primaryButton(text: string) {
  .buttonBase()  // 复用基础样式
  .backgroundColor('#FF6B35')
  .fontColor(Color.White)
  .stateEffect(true)
}

@Extend(Button) function secondaryButton(text: string) {
  .buttonBase()
  .backgroundColor(Color.White)
  .fontColor('#FF6B35')
  .border({ width: 1, color: '#FF6B35' })
  .stateEffect(true)
}

@Extend(Button) function disabledButton(text: string) {
  .buttonBase()
  .backgroundColor('#E5E5EA')
  .fontColor('#8E8E93')
  .enabled(false)
}

@Extend(Button) function smallButton(text: string) {
  .width(80)
  .height(32)
  .type(ButtonType.Capsule)
  .fontSize(12)
  .borderRadius(16)
  .backgroundColor('#007AFF')
  .fontColor(Color.White)
}

// 重构后的组件
@Component
struct ProductDetailNew {
  build() {
    Column({ space: 12 }) {
      // 统一调用,清晰一致
      Button().primaryButton('立即购买')
      Button().secondaryButton('加入购物车')
      Button().disabledButton('已售罄')
      Button().smallButton('收藏')
    }
  }
}

3. 效果对比

维度 改造前 改造后 提升
代码行数 45行 15行 -67%
维护点 3个独立样式 1个设计系统 -67%
一致性 容易出错 100%保证 +∞
新按钮开发 从头写 调用现有方法 秒级

更重要的是,当设计总监说"所有按钮圆角改成24px"时,你只需要改buttonBase里的一个数字。


五、与@Styles的详细对比:什么时候用哪个?

很多人纠结这个问题,其实有个简单的判断标准:

typescript 复制代码
// 场景1:所有组件都有的通用属性 → 用@Styles
@Styles function shadowCard() {
  .width(200)
  .height(100)
  .backgroundColor(Color.White)
  .borderRadius(8)
  .shadow({ radius: 4 })  // 所有组件都有shadow
}

// Text能用,Button也能用,Column也能用
Text('文本').shadowCard()
Button('按钮').shadowCard()
Column().shadowCard()

// 场景2:特定组件的私有属性 → 必须用@Extend
@Extend(Text) function textLink() {
  .fontColor('#007AFF')
  .decoration({ type: TextDecorationType.Underline })  // Text私有属性
  .onClick(() => { /* 点击事件 */ })
}

// 只有Text能用
Text('链接').textLink()  // 
// Button('按钮').textLink()  //  编译错误!

// 场景3:需要参数 → 必须用@Extend
@Extend(Image) function responsiveImage(maxWidth: number) {
  .width(maxWidth)
  .aspectRatio(1)  // 保持1:1
  .objectFit(ImageFit.Cover)  // Image私有属性
}

// 场景4:组件内部复用 → 用@Styles
@Component
struct MyComponent {
  @Styles internalStyle() {  // 只能在组件内用
    .width(this.componentWidth)
    .height(this.componentHeight)
  }
  
  build() {
    Column().internalStyle()
  }
}

// 场景5:全局复用 + 参数 + 私有属性 → @Extend是唯一选择
@Extend(Button) function smartButton(
  text: string,
  isPrimary: boolean = true,
  isLoading: boolean = false
) {
  .type(ButtonType.Capsule)
  .width(120)
  .height(44)
  .backgroundColor(isPrimary ? '#007AFF' : Color.White)
  .fontColor(isPrimary ? Color.White : '#007AFF')
  .stateEffect(!isLoading)
  .enabled(!isLoading)
  
  if (isLoading) {
    .fontColor('#999999')
  }
}

简单决策树

  1. 需要传参数吗? → 是 → 用@Extend
  2. 需要访问组件私有属性吗? → 是 → 用@Extend
  3. 只在单个组件内部用吗? → 是 → 用@Styles
  4. 所有组件都能用吗? → 是 → 用@Styles
  5. 其他情况 → 优先用@Extend(更灵活)

六、鸿蒙6新特性:@Extend的"超进化"

HarmonyOS 6给@Extend带来了几个让人兴奋的增强:

1. 支持泛型组件扩展

typescript 复制代码
// HarmonyOS 6+:可以扩展泛型组件
@Extend(ForEach) function gridLayout<T>(
  items: Array<T>,
  columns: number = 3
) {
  .gridContainer({
    columnsTemplate: `repeat(${columns}, 1fr)`,
    rowsGap: 12,
    columnsGap: 12
  })
}

// 使用:类型安全的网格布局
@Component
struct ProductGrid {
  private products: Product[] = [...]
  
  build() {
    ForEach(this.products, (product: Product) => {
      ProductItem({ product: product })
    })
    .gridLayout(this.products, 4)  // 4列网格
  }
}

2. 条件样式支持

typescript 复制代码
// 更灵活的条件逻辑
@Extend(Text) function statusText(
  status: 'success' | 'warning' | 'error',
  size: number = 14
) {
  .fontSize(size)
  .fontWeight(FontWeight.Medium)
  
  // 条件样式(鸿蒙6优化了编译支持)
  if (status === 'success') {
    .fontColor('#34C759')
    .decoration({ type: TextDecorationType.None })
  } else if (status === 'warning') {
    .fontColor('#FF9500')
    .decoration({ type: TextDecorationType.Underline })
  } else {
    .fontColor('#FF3B30')
    .decoration({ type: TextDecorationType.LineThrough })
  }
}

// 使用
Text('支付成功').statusText('success')
Text('库存不足').statusText('warning')
Text('已下架').statusText('error')

3. 组合式API支持

typescript 复制代码
// 结合新的组合式API
@Extend(Column) function pageContainer(
  useSafeArea: boolean = true,
  backgroundColor: Color = Color.White
) {
  .width('100%')
  .height('100%')
  .backgroundColor(backgroundColor)
  .padding(16)
  
  if (useSafeArea) {
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  }
}

// 使用:声明式+组合式
@Entry
@Component
struct HomePage {
  build() {
    Column()
      .pageContainer(true, '#F2F2F7')
  }
}

4. 性能优化:编译时类型检查增强

鸿蒙6的编译器对@Extend做了深度优化:

  • 类型安全:编译时检查参数类型和组件属性匹配
  • 树摇优化:未使用的@Extend方法自动移除
  • 内联优化:简单的方法调用直接内联展开
typescript 复制代码
// 编译前
@Extend(Button) function simpleButton() {
  .width(100).height(44)
}

Button().simpleButton()

// 编译后(可能直接内联)
Button().width(100).height(44)

七、避坑指南:@Extend的那些"坑"与"填"

1. 作用域陷阱

typescript 复制代码
// 错误:在@Extend里访问组件状态
@Component
struct MyComponent {
  @State privateSize: number = 100
  
  build() {
    // 这行没问题
    Text('测试').responsiveText(this.privateSize)
  }
}

@Extend(Text) function responsiveText(size: number) {
  .fontSize(size)
  // .fontSize(this.privateSize) // 不能访问组件状态!
}

// 正确:通过参数传递
@Extend(Text) function responsiveText(size: number) {
  .fontSize(size)
}

// 使用
Text('测试').responsiveText(this.privateSize)  // 参数传递

2. 循环依赖问题

typescript 复制代码
//  错误:@Extend方法相互调用形成死循环
@Extend(Text) function styleA() {
  .fontSize(16)
  .styleB()  // 调用B
}

@Extend(Text) function styleB() {
  .fontColor(Color.Red)
  .styleA()  // 又调回A,无限循环!
}

//  正确:单向依赖链
@Extend(Text) function baseStyle() {
  .fontFamily('HarmonyOS Sans')
  .lineHeight(1.5)
}

@Extend(Text) function titleStyle(size: number) {
  .baseStyle()  //  单向调用
  .fontSize(size)
  .fontWeight(FontWeight.Bold)
}

@Extend(Text) function bodyStyle() {
  .baseStyle()  //  也调用base,但不会循环
  .fontSize(14)
  .fontColor('#666')
}

3. 过度设计反模式

typescript 复制代码
// 错误:为每个微小变化都创建@Extend
@Extend(Button) function redButton() { .backgroundColor(Color.Red) }
@Extend(Button) function blueButton() { .backgroundColor(Color.Blue) }
@Extend(Button) function greenButton() { .backgroundColor(Color.Green) }
@Extend(Button) function smallRedButton() { .backgroundColor(Color.Red).width(80) }
@Extend(Button) function largeRedButton() { .backgroundColor(Color.Red).width(160) }

// 结果:几十个方法,谁也记不住谁是谁

// 正确:参数化设计
@Extend(Button) function coloredButton(
  color: Color,
  size: 'small' | 'medium' | 'large' = 'medium'
) {
  const sizes = {
    small: { width: 80, height: 32, fontSize: 12 },
    medium: { width: 120, height: 44, fontSize: 16 },
    large: { width: 160, height: 56, fontSize: 18 }
  }
  
  const config = sizes[size]
  .width(config.width)
  .height(config.height)
  .fontSize(config.fontSize)
  .backgroundColor(color)
  .fontColor(getContrastColor(color))  // 自动计算对比色
}

// 使用:清晰直观
Button().coloredButton(Color.Red, 'medium')
Button().coloredButton(Color.Blue, 'small')
Button().coloredButton(Color.Green, 'large')

八、最佳实践:@Extend与其他技术的默契配合

1. 与@Builder的分工协作

typescript 复制代码
// @Builder负责结构,@Extend负责样式
@Builder function productCard(product: Product) {
  Column({ space: 12 }) {
    // 结构部分
    Image(product.image)
      .productImage()  // @Extend
    
    Column({ space: 8 }) {
      Text(product.name)
        .productTitle()  // @Extend
      
      Text(`¥${product.price}`)
        .productPrice()  // @Extend
      
      Button('购买')
        .primaryButton()  // @Extend
    }
  }
  .cardContainer()  // @Extend
}

// 样式定义
@Extend(Image) function productImage() {
  .width(120).height(120).borderRadius(8).objectFit(ImageFit.Cover)
}

@Extend(Text) function productTitle() {
  .fontSize(16).fontColor('#1C1C1E').fontWeight(FontWeight.Medium).maxLines(2)
}

@Extend(Text) function productPrice() {
  .fontSize(18).fontColor('#FF6B35').fontWeight(FontWeight.Bold)
}

@Extend(Column) function cardContainer() {
  .width(160).padding(12).backgroundColor(Color.White).borderRadius(12).shadow({ radius: 4 })
}

2. 与AttributeModifier的强强联合

typescript 复制代码
// AttributeModifier用于运行时动态样式,@Extend用于编译时静态样式
class ThemeModifier implements AttributeModifier<ButtonAttribute> {
  private isDark: boolean
  
  constructor(isDark: boolean) {
    this.isDark = isDark
  }
  
  applyNormal(state: ButtonAttribute) {
    state.backgroundColor = this.isDark ? '#1C1C1E' : Color.White
    state.fontColor = this.isDark ? Color.White : '#1C1C1E'
  }
}

// @Extend定义基础样式
@Extend(Button) function themeAwareButton(text: string) {
  .width(120)
  .height(44)
  .type(ButtonType.Capsule)
  .borderRadius(22)
  .fontSize(16)
}

// 使用:静态+动态结合
@Component
struct ThemeButton {
  @StorageProp('isDarkMode') isDark: boolean = false
  
  build() {
    Button()
      .themeAwareButton('切换主题')  // @Extend:静态样式
      .attributeModifier(new ThemeModifier(this.isDark))  // 动态样式
      .onClick(() => {
        AppStorage.set('isDarkMode', !this.isDark)
      })
  }
}

3. 与状态管理的深度集成

typescript 复制代码
// 结合AppStorage实现全局样式主题
@Extend(Text) function themedText(
  type: 'title' | 'body' | 'caption',
  useGlobalTheme: boolean = true
) {
  // 获取全局主题配置
  const theme = useGlobalTheme ? AppStorage.get('theme') : 'light'
  const isDark = theme === 'dark'
  
  // 根据类型和主题设置样式
  if (type === 'title') {
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .fontColor(isDark ? Color.White : '#1C1C1E')
  } else if (type === 'body') {
    .fontSize(14)
    .lineHeight(20)
    .fontColor(isDark ? '#CCCCCC' : '#666666')
  } else {
    .fontSize(12)
    .fontColor(isDark ? '#999999' : '#8E8E93')
  }
}

// 使用:自动响应主题变化
@Component
struct ThemedComponent {
  @StorageProp('theme') currentTheme: string = 'light'
  
  build() {
    Column() {
      Text('页面标题')
        .themedText('title')  // 自动使用当前主题
      
      Text('正文内容...')
        .themedText('body')
      
      Text('2024年1月1日')
        .themedText('caption')
    }
  }
}

九、性能考量:@Extend真的"零成本"吗?

很多人担心@Extend会影响性能,其实不然------但有些细节需要注意。

1. 编译时成本 vs 运行时成本

typescript 复制代码
// 编译前
@Extend(Button) function fancyButton() {
  .width(100).height(44).backgroundColor(Color.Blue)
}

Button().fancyButton()

// 编译后(伪代码)
Button().width(100).height(44).backgroundColor(Color.Blue)

@Extend在编译时 被展开,运行时 就是普通的属性设置,所以运行时零开销。但编译过程需要做类型检查和原型扩展,大项目里可能会有轻微影响。

2. 方法调用链的优化

typescript 复制代码
// 深层嵌套的方法调用
@Extend(Text) function a() { .fontSize(10) }
@Extend(Text) function b() { .a().fontColor(Color.Red) }
@Extend(Text) function c() { .b().backgroundColor(Color.White) }
@Extend(Text) function d() { .c().margin(10) }

Text('测试').d()  // 调用链: d → c → b → a

// 编译优化后可能变成
Text('测试').fontSize(10).fontColor(Color.Red).backgroundColor(Color.White).margin(10)

鸿蒙6的编译器会对方法调用链做扁平化优化,减少函数调用开销。

3. 内存占用考量

因为@Extend方法是挂载在组件原型上的,所以所有实例共享同一份方法,不会每个实例都创建新函数。内存占用可以忽略不计。

实际建议

  • 对于高频更新的动态样式,考虑用状态变量直接控制
  • 对于稳定的、多处复用的样式,大胆用@Extend
  • 避免创建过多细碎的@Extend方法(超过50个可能影响可读性)
  • 复杂的条件逻辑考虑拆分成多个简单方法

十、总结一下下:@Extend的"道"与"术"

用了这么久@Extend,我最大的体会是:它改变的不是代码写法,而是设计思维

以前写样式,是"这个Text要红色,那个Button要蓝色"------关注的是具体效果。现在写样式,是"定义一套Text的扩展方法,一套Button的扩展方法"------关注的是组件能力扩展

三个核心原则

  1. 单一职责:一个@Extend方法只扩展一个组件类型。别想着一个方法既管Text又管Button。
  2. 参数化设计 :能用参数解决的,就不要创建多个方法。coloredButton(Color.Red)redButton()更灵活。
  3. 命名即文档productTitletextStyle1好,primaryButtonbtnStyleA好。

两个实用技巧

  1. 建立组件样式库 :按组件类型组织@Extend方法,比如TextStyles.etsButtonStyles.ets
  2. 版本控制友好:@Extend方法集中管理,Git diff清晰,代码审查方便。

一个终极建议

下次写组件样式前,先问自己:这个样式逻辑会在同类型组件中复用吗?如果需要参数或访问私有属性吗?如果答案是"是",那就用@Extend封装起来。

记住,好的架构不是设计出来的,是重构出来的。而@Extend,就是你构建可维护、可扩展UI系统时最趁手的工具。


最后的 checklist

  • 需要访问组件私有属性吗? → @Extend
  • 需要传参数吗? → @Extend
  • 只在当前文件用吗? → 全局@Extend
  • 方法名能一眼看懂用途吗?
  • 参数类型定义清晰吗?
  • 避免循环调用了吗?
  • 编译报错?检查组件类型和属性支持

从今天起,让你的样式代码告别重复劳动,拥抱@Extend的灵活与强大。毕竟,时间应该花在创造价值上,而不是在几十个文件里复制粘贴同样的fontColorborderRadius

相关推荐
nashane5 小时前
HarmonyOS 6学习:旋转动画优化与长截图性能调优——打造丝滑交互体验的深度实践
学习·交互·harmonyos·harmonyos 5
南村群童欺我老无力.10 小时前
鸿蒙自定义组件接口设计的向后兼容陷阱
华为·harmonyos
liulian091611 小时前
Flutter 跨平台路由与状态管理:go_router 与 Riverpod 的 OpenHarmony总结
flutter·华为·学习方法·harmonyos
liulian091612 小时前
Flutter for OpenHarmony 跨平台技术实战:flutter_animate 与 pull_to_refresh 库的鸿蒙化适配总结
flutter·华为·学习方法·harmonyos
南村群童欺我老无力.12 小时前
鸿蒙PC开发的路由导航参数传递的类型安全陷阱
安全·华为·harmonyos
IntMainJhy13 小时前
【flutter for open harmony】第三方库 Flutter 二维码生成的鸿蒙化适配与实战指南
数据库·flutter·华为·sqlite·harmonyos
jiejiejiejie_14 小时前
Flutter for OpenHarmony 底部选项卡与多语言适配小记:让 App 更贴心的两次小升级✨
flutter·华为·harmonyos
轻口味14 小时前
HarmonyOS 6.1 全栈实战录 - 01 沉浸式视效探索:HDS 下的“光感”交互引擎深度解析与实践
华为·harmonyos
jiejiejiejie_15 小时前
Flutter for OpenHarmony 应用更新检测与萌系搜索功能实战小记✨
flutter·华为·harmonyos
IntMainJhy15 小时前
Flutter 三方库 Firebase Messaging 鸿蒙化适配与实战指南(权限检查+设备Token获取全覆盖)
flutter·华为·harmonyos