鸿蒙开发:一次ArkUI之旅

阅读难度:简单(时间:20240306)

前言:

有幸参与到一个鸿蒙项目的开发,一边开发一边学习总结。
本文主要介绍状态绑定的使用与项目开发过程中遇到的问题及思考这类通用性的内容,不对具体的技术细节进行探讨,精力有限,官方文档更新也很频繁,具体细节可参考官方文档

【ArkTS】:HarmonyOS 优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript生态基础上做了进一步扩展
【ArkUI】:HarmonyOS 下的一套声明式UI


UI 相关

如果有声明式UI开发的经验(如Flutter),那么鸿蒙开发就能事半功倍,如果精通 SwiftUI 及其主要机制,粗略的看下文档就能进入开发。

声明式UI的优点在于 易理解,组合方便,UI维护方便,上手简单。缺点在复杂页面上可能出现性能问题,不易排查优化,且学习曲线较为陡峭。

这次整个团队基本都是从零开始学习鸿蒙开发,印象最深的应该是:部分同学对页面渲染流程认知较少

前端初学者及部分多年经验工作者都存在该问题,在声明式UI开发中更加明显的暴露出来,初学者直接上手 声明式UI 会有一个渲染理解的过程。一个页面,对于前端开发,就是一个视图树,而渲染引擎会根据视图树来进行优化、绘制:

视图树举例

实际开发中的视图树规模会大得多,屏幕上看到的视图大部分都靠近视图树的末端。对视图的更改(例如:增删改查)本质上是对视图树、或通过视图树进行操作。

在将视图特性都 样板化内敛 的声明式UI框架中,初学者能快速上手、并且做出性能不差的页面。但这不能成为降低在开发中对性能关注度的理由。

相反,在视图的层级结构上一定要有个清晰的认知,这样才能在出现性能、渲染、逻辑类问题时快速定位问题,并找出较优的解决方案,能够独自解决下面类似的问题:

  1. 为什么这个XX没达到预期效果?
  2. 在XX场景下的性能问题如何优化?

前端开发正式入门的里程碑之一应当包含:
大脑就是一台【渲染引擎】


双向绑定

在MVVM中,双向绑定很常见,不同的UI框架中实现方式不同,原理类似。iOS 可参考旧文(初见CombineSwiftUI 中的 @State、@StateObject、@ObservedObject

属性包装器也可唤做“装饰器”、“注解”,下文与官方文档中保持统一,使用“属性装饰器”代指)

【ArkUI中常见的属性装饰器简介】:
@Entry : 自定义页面入口 (Page,可实现onPageShow等页面生命周期方法)
@Component : 自定义组件 (View,需实现build方法)
@State : 状态变量(持有、双向)
@Prop: 状态变量(深拷贝,注意性能!!!、单向) 必须初始化
@Link: 状态变量(绑定、双向)
@Provide : 跨组件双向同步 (需配合 @Consume 使用,类似SwiftUI中的环境值)
@Observed : 装饰Class, 需配合 @ObjectLink、@Prop使用 (注意, 并非对标 SwiftUI中 iOS 17新增的Observable, 无多层级观察! 是iOS 17之前那种样板代码, 多层级嵌套的class每个都要用@Observed标识)
@ObjectLink : 配合@Observed双向绑定
@Reusable : 标识自定义组件可复用
@Builder : 标识方法为视图构造器 (默认值传递、$$强制引用传递可响应@State, 可参考Swift中Combine的$)
@BuilderParam : UI 参数 (配合@Require 可强制父组件传参)
@Styles : 用于样式复用、不支持参数 (组件内声明可访问this)
@Extend : 扩展组件样式、支持参数(仅可全局)
@AnimatableExtend: 可动画属性(仅可全局、参数必须为number类型或实现了AnimtableArithmetic<T>的自定义类型、只可调用括号内组件的方法)
@Require : 检验 @Prop 和 @BuilderParam是否需要构造传参 (不可单独使用)
@Watch : 状态变量更改通知, 属性包装起顺序放最内侧 (调试?)
@Track : 装饰class对象的单个属性以可观测

各装饰器的更详细介绍和使用方式可参考官方文档

以状态绑定最常用到的 @State、@Prop、@Link、@Observed、@ObjectLink以及@Provide、@Consume来举个例子:


/// 数据
@Observed
export class MainViewModel {
    sectionModel?: SectionModel = new SectionModel()
    someNum: number = 0
    
    runEvent(eventType:EventType) {
        ......
    }
}

@Observed
export class SectionModel {
    title?: string
    someText?: string
}

/// 页面
@Entry
@Component
export struct MainPage {
    @Provide viewModel: MainViewModel = new MainViewModel()
    
    build() {
        Column() {
            SectionCardView({cardModel: this.viewModel.cardModel})
        }
    }
}

/// 自定义组件
@Component
export struct SectionCardView {
    @ObjectLink cardModel: SectionModel
    
    @State backOpacity: number
    @State positionY: number | string = '100%'
    
    build() {
        Row() {
            SectionCardView({backOpacity: this.backOpacity})
        }
        .opacity(this.backOpacity)
        .position({y: this.positionY})
        .onAppear(() => {
            this.showAnim()
        })
    }
    
    showAnim() {
        animateTo({
        duration: 300,
        curve: Curve.EaseInOut,
        playMode: PlayMode.Alternate
    },() => {
        this.backOpacity = 1
        this.positionY = ''
    })
  }
}

/// 自定义组件
@Component
export struct EventCardView {
    @Link backOpacity: number
    
    @Consume viewModel: MainViewModel
    
    build() {
      Button({type: ButtonType.Normal}) {
        Text('触发事件')
      }
      .width(20)
      .backgroundColor(Color.White)
      .opacity(this.backOpacity)
      .onClick((event: ClickEvent) => {
         this.viewModel.runEvent(EventType.TestEvent)
      })
    }
}

这是一个有着三层视图的示例,仅保留了属性装饰器部分以供理解参考,从代码可知:

  1. MainViewModel 与 SectionModel 都被 @Observed 装饰器修饰,它们的属性变化可被观察。
  2. MainPage 与子视图都被 @Component 修饰为自定义组件,意味着都必须实现视图构建方法:build()。不同的是 MainPage 还被 @Entry 修饰,标识其同时作为一个页面的入口,可供路由使用。
  3. MainPage 下 的viewModel并未被 @State 标识, 而是使用了有着跨层级访问特点的 @Provide
    【Tips】:
    这是因为 ViewModel 贯穿着整个页面的逻辑处理。采用@State + @ObjectLink的方式在实际开发中存在着很多问题:层层传递极大的增加了样板代码,同时使得部分业务逻辑排查难度指数增加,在多人开发项目中VM层过于开放。使用 @Provide 可省去这些麻烦。
  4. SectionCardView 中使用了 @ObjectLink 来对外部传入的数据进行双向绑定。
  5. SectionCardView 中的动画属性 positionY、backOpacity 使用了 @State 标识,因为外部对该属性不敏感,也表明 SectionCardView 持有这两个属性,当进行动画操作时,自动更新与其绑定的视图。
  6. EventCardView 中,使用 @Link 来双向绑定父视图 SectionCardView 的 backOpacity 属性。
  7. EventCardView 中 使用 @Consume 来跨层级获取viewModel, 并在点击时,调用触发 viewModel 的 runEvent 方法。
    【Tips】:
    通常情况下 @Consume 需与 @Provide 的属性名一致来进行匹配。也可使用别名来匹配,如:@Provide('vm') a@Consume('vm') b
【一个开发中的小插曲】:“怎么视图没有自动刷新?”

某个模块的视图结构类似下面这样:

ForEach() {
   ForEach() {
       ForEach() {
          // 此处的数据更新没有刷新页面???”
       }
   }
}

数据更新没问题,三层相关的数据也都有用ArrayObserve修饰。视图层的数据结构类似三维数组..直接改动各层数组元素的属性却有着差别,最外层可响应,中层数据不会刷新,最内层直接无响应。
原因:官方文档介绍ForEach会响应绑定的List个数变更,也会响应具体元素下的属性变更(通过@objectLink绑定),笔者想当然的认为在这种多层ForEach下能够层层传递。
结论:数据长度不变时,ForEach的刷新由键值生成函数keyGenerator控制,最外层ForEach具体元素的属性能变化是因为变化由@ObjectLink告知了视图,可当内层又是一个ForEach时,内层ForEach的刷新受键值变化控制,所以直接改内层的属性值就不会刷新。那么第三层的ForEach自然更不会有反应。
处理:给ForEach所持有的数组元素对象下添加唯一标识ID(本该如此),当数据更新,或需要强制更新对应视图时,更新其标识ID,而这在内层的ForEach上的开销微乎其微。

架构选择

大部分页面都是MVVM架构(永远的神!),有的模块因为业务最为复杂,逻辑联动非常多,借鉴了前段时间开发 VisionPro 时的经验,继续使用 MVI(MVVM变种) 架构:

MVI.png

架构没有最好、只有最适合:适合业务,适合技术栈,适合团队

着重说下“适合团队”,这次鸿蒙工程的开发过程中,团队二十多人,iOS、Android、Flutter、H5、Rust 什么技术栈都有,若对声明式 UI 没有任何经验,使用MVVM 本身就存在学习成本,对从未接触过声明式UI的同学这个成本并不低。

感兴趣的朋友可以看看曾经笔者的MVI偶遇

结语

接触到的声明式UI开发中,鸿蒙的开发体验比 Flutter 好很多,与 SwiftUI 的差距还比较明显,不过官方更新迭代非常快,并且 ArkUI 与 SwiftUI 一样,都覆盖是其旗下全平台产品,期待后期的发展。

等有空了再搞一篇完整的自定义组件开发吧 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容