HarmonyOS,中文称鸿蒙系统或鸿蒙OS。虽然已经广为流传,但很多人对鸿蒙系统还存在误解,以为鸿蒙其实类似于Android、iOS只是给手机、平板用的移动操作系统。其实这个说法不全面,鸿蒙系统是华为推出的面向全场景的分布式操作系统。所谓面向全场景是指它将来不光是给予手机、平板搭载,而是会对诸如电脑、车机、手表、电视、音响、眼镜及各种家电家居等等多设备都能搭载的操作系统。这就是华为所宣导的全场景智慧生活战略1+8+N(手机+8种常用设备+万物,共同协作共同参与)
因此,国内厂商为了覆盖这么多终端,必然需要对应的应用,所以鸿蒙系统将带来大量岗位。待到9.30左右HarmonyOSNext发布正式版面向普通用户,绝对是爆发之时。
总体说来,HarmonyOS的特点,其实华为提炼出了三大特征:
一次开发,多端部署
可分可合,自由流转
统一生态,原生智能
言归正传:华为又是怎么让实现一次开发,多端部署的呢?
意思就是:HarmonyOS支持在不同设备之间的无缝切换和协作。用户可以在一个设备上开始任务,然后很方便地转移到另一个设备上继续操作,非常类似于iphone的appleId,通过绑定设备间的关系,来保存用户的操作状态。
这种特性增强了设备间的协同工作能力,提供了更流畅的用户体验。例如,用户可以在手机上查看内容,然后在电视上继续观看,或者在平板上编辑文档后在电脑上进行进一步处理。
(这就是任老爷子强调的“生态”)
HarmonyOS构建了一个统一的生态系统,使得所有兼容设备能够协同工作,互联互通。同时,它也集成了原生的智能技术,比如AI能力,用于提升设备的智能水平。
统一生态使得用户可以在各种设备上享受一致的体验,而原生智能技术能够提供更智能的服务和功能,如智能推荐、语音识别等,增强了设备的智能化和个性化。
所以个人认为认为鸿蒙可能会蚕食安卓的其中一部分原因就是因为鸿蒙的这三大特性。这三大特性不是安卓实现不了,而是安卓能实现,但不一定在设备上有统一标准,毕竟安卓现在太碎片化,各厂商有自己的深度定制,很难形成统一标准。更何况鸿蒙依托国内环境,在国家号召核心技术自主化的大背景下,更具有地利。
介绍完HarmonyOS,我们正式进入开发学习阶段。
按照官网上的说法,ArkTS是HarmonyOS的主力应用开发语言。
”工欲善其事,必先利其器”,我们先来简单了解一下ArkTS与TypeScrip:
TypeScript的语法非常简单,有过Java、Kotlin、Dart等语言开发经验的小伙伴,会非常容易上手。但也有些特殊之处,我个人认为值得拿出来讲一讲:
类型注解是TypeScript的核心特性之一,它允许在变量、函数参数和函数返回值上添加类型信息。这有助于在编译时发现和修复类型错误。
示例:
letmessage:string="Hello,HarmonyOS";letcount:number=10;functiongreet(name:string):string{return`Hello,${name}`;}letgreeting:string=greet("HarmonyOS");2.接口接口是TypeScript中定义复杂类型的一种方式,接口可以用于类型检查,确保对象符合预期的结构。我认为值得一讲的原因是:它可以描述一个对象的结构。(这个比较重要,在后续的开发中经常用到)
与其他编程语言一样,ArkTS支持函数定义与调用,比较不同的是:ArkTS支持箭头函数,这是一种比普通函数更加简洁的函数写法,最大的优点就在于它极简
letlogSomething=()=>{console.log("遥遥领先")}logSomething()4.对象方法即属于对象的方法,描述对象的具体行为,可以与我们上述的箭头函数一起配合使用
interfaceBlackWuKong{attack:()=>void,records:(action:string)=>void}letMalou:BlackWuKong={attack:():void=>{console.log("敲")},records:(action:string):void=>{console.log("存档","上香",action)}}5.对象数组//定义一个Person类classPerson{name:string;age:number;constructor(name:string,age:number){this.name=name;this.age=age;}getDetails():string{return`${this.name}is${this.age}yearsold.`;}}//创建Person对象数组constpeople:Person[]=[newPerson("Alice",30),newPerson("Bob",25),newPerson("Charlie",35)];//遍历对象数组并输出每个对象的详情people.forEach(person=>{console.log(person.getDetails());});6.联合类型联合类型是一种灵活的数据类型,它修饰的变量可以存储不同类型的数据,这个相比于大部分的编程语言算是一种小创新了,解决我们很多时候数据传递时候泛型不统一的痛点。
语法:let变量:类型1|类型2|类型3=值
lettrackId:number|string=12508trackId="12580"//即使初始化是number类型,也可以在后续修改成string类型示例:
//math.tsexportfunctionadd(x:number,y:number):number{returnx+y;}exportfunctionsubtract(x:number,y:number):number{returnx-y;}//app.tsimport{add,subtract}from'./math';console.log(add(10,5));//Output:15console.log(subtract(10,5));//Output:58.异步编程ArkTS支持Promise和async/await语法,使得异步编程变得更加简洁明了。
letresponse=awaitfetch(url);使用fetch函数发起对给定url的网络请求。await关键字使得函数在这个异步操作完成之前暂停执行,一旦请求完成,response变量将包含服务器的响应。
letdata=awaitresponse.json();在接收到响应后,调用response.json()方法将响应体解析为JSON对象。同样,await确保在解析完成之前函数不会继续执行。最后,函数返回解析后的JSON数据。
.then(data=>console.log(data))如果网络请求成功并且数据解析成功,这个回调函数会被调用,并将解析后的数据作为参数传入,这里只是简单地将数据打印到控制台。
⑥.catch(error=>console.error(error))如果在网络请求或者数据解析过程中出现任何错误,这个回调函数会被调用,并将错误对象作为参数传入,这里将错误信息打印到控制台以便进行调试。
类型别名允许你为现有类型创建一个新的名字。这对于创建复杂类型或提高代码可读性非常有用。
typePoint={x:number;y:number;};functiondrawPoint(point:Point):void{console.log(`Drawingpointat(${point.x},${point.y})`);}letpoint:Point={x:10,y:20};drawPoint(point);10.类型保护类型保护是一种检查变量类型的方法,可以在编译时确保变量具有正确的类型。这对于处理联合类型或处理类型转换时非常有用。
typeShape=Circle|Square;functiongetArea(shape:Shape):number{if(shapeinstanceofCircle){returnMath.PI*shape.radius**2;}else{returnshape.width*shape.height;}}11.映射类型映射类型允许你根据现有类型创建新的类型,例如将一个对象的所有属性设置为只读或可选。这在处理现有类型时非常有用,可以避免创建重复的类型定义。
进行开发之前我们先来熟悉一下工程目录:
├──AppScope│└──entry#HarmonyOS工程模块,编译构建生成一个HAP包│├──src││├──main#应用/服务的入口││├──pages#应用/服务包含的页面(写页面逻辑的地方)│││└──Index.ets#页面源码文件││└──resources#存放应用/服务所用到的资源文件│└──module.json5#模块配置文件2、剖析ArtTs的UI范式语法在正式开发前,我们先简单学习一下默认模板的代码,熟悉一下ArtTS的UI范式语法
@Entry@ComponentstructHelLo{@StatemyText:string='wortd'build(){Column(){Text('Hello').fontsize(50)Text(this.myText).fontsize(50)Divider()Button(){Text('CLickme').fontsize(30).onClick(()=>this.myText='ArkUI')}width(200).height(50)}}}我们先来简单剖析一下这段代码:
系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Column、Text、Divider、Button。
属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。
导读:关于装饰器和组件等知识我们会在后面做更详细的说明和验证,这里只是先让大家简单的了解和熟悉我们的开发页面和UI范式语法
同android开发一样,华为鸿蒙官方也提供了很多基础组件,下面介绍几种容器组件和常用基础组件的使用方式。
Column竖向排列容器组件,等同于竖向的LinerLayout。Row横向排列的容器组件,等同于横向的LinerLayou。使用方法如下:
Column(){Text("1")...Text("2")...Text("")...}Row(){Text("1")...Text("2")...Text("3")...}关键属性:
RleativeContainer相当于android中的RelativeLayout,使用方式如下:
@ComponentexportstructListUi{@Statearr:string[]=["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19"]build(){Column(){List({space:20,initialIndex:0}){ForEach(this.arr,(item)=>{ListItem(){Text(''+item).width('100%').height(100).fontSize(16).textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)}.border({width:2,color:Color.Green})},item=>item)}.height("100%").width("100%").padding(20)}}}效果如下:
关键属性和接口:
Grid网格布局组件,同GridView,简单使用方式如下:
我们可以发现List和Grid都不再需要通过Adapter来实现数据的绑定了,而是通过forEeach渲染来实现多item的渲染,我们来简单了解一下forEach渲染机制。
理解ForEach渲染控制:
基本概念:
在鸿蒙开发中,ForEach是一种用于遍历数组或可迭代对象并动态渲染组件的机制,若需要根据数组数据生成一堆同类型的组件,则需要用到ForEach,根据数组生成组件,数组有多少个元素,就生成多少个组件
结构语法:
ForEach(arrayOrIterable,(element,index)=>{//返回一个组件实例returnComponentForEach(element,index);})arrayOrIterable:要遍历的数组或可迭代对象。element:当前遍历到的元素。index:当前元素的索引。工作原理
数据绑定:ForEach会自动监测绑定的数组或可迭代对象的变化。如果数据源发生改变(例如添加、删除或修改元素),ForEach会相应地更新界面上的组件。
组件生成:对于数据源中的每个元素,都会调用提供的函数,并传入当前元素和索引。这个函数应该返回一个组件实例,该组件将被渲染在界面上。
性能优化:鸿蒙的渲染系统会尽量高效地处理ForEach的渲染。它会尝试最小化不必要的重新渲染,只更新那些由于数据源变化而受影响的组件。
使用场景
列表渲染:当需要展示一个列表时,比如展示一组商品、联系人或消息,可以使用ForEach来遍历数据源并为每个元素创建一个列表项组件。
动态界面生成:如果界面的一部分需要根据动态数据来生成不同数量的组件,可以使用ForEach根据数据的变化实时更新界面。
注意事项
数据源稳定性:确保数据源在渲染过程中不会被意外修改,否则可能会导致不可预测的渲染结果。如果需要修改数据源,最好在合适的时机进行,并确保界面能够正确地响应这些变化。
性能考虑:虽然ForEach会进行一定的性能优化,但如果数据源非常大,可能会对性能产生影响。在这种情况下,可以考虑分页加载数据或使用其他优化技术。
索引的使用:索引参数可以在某些情况下很有用,比如为每个组件设置唯一的key属性,以帮助渲染系统更高效地进行更新。
在App效果中,我们经常看到一些阴影蒙版、加载中遮罩、悬浮小窗口等,如下图列表所示:
像这种:把某个组件堆叠到另外一个组件上的效果,称之为层叠效果或者堆叠效果(就像小时候玩的叠罗汉,一个人叠在另一个人上面)
使用语法
Stack(){item1()item2()item3()}默认情况下:各个item居中对齐,越在后面的组件越叠在最高层,如图所示:
也可以通过修改Stack的alignContent属性,得到下面的效果:
修改方法
Stack({alignContent:Alignment.TopStart}){//改成在左上对齐堆叠.....}如果需要手动指定谁在最上层,可以使用zIndex属性,修改层级。zIndex的值越大越叠在最上面
@Entry@ComponentexportstructStackUi{build(){Stack(){Text('item1').fontSize(12).fontColor('#fff').fontWeight(FontWeight.Bold).width('100%').height('100%').backgroundColor('rgba(0,0,255,0.2)').textAlign(TextAlign.Start).align(Alignment.Top).zIndex(3)Text('item2').fontSize(12).fontColor('#fff').fontWeight(FontWeight.Bold).width('60%').height('60%').backgroundColor('rgba(0,255,0,0.2)').textAlign(TextAlign.Start).align(Alignment.Top).zIndex(1)Text('item3').fontSize(12).fontColor('#fff').fontWeight(FontWeight.Bold).width('30%').height('30%').backgroundColor('rgba(255,255,0,0.2)').textAlign(TextAlign.Start).align(Alignment.Top).zIndex(2)}.alignContent(Alignment.BottomEnd).width("100%").height("40%").backgroundColor('rgba(8,8,8,0.2)')}}效果:
可以看到item2和item3的字体颜色看起来是有一些被盖着的,因为item1是处于最上层
这个控件是个开发都能知道是显示文本的控件,不必多说
Text('开始学习HarmonyOSNEXT').fontSize(22).fontColor('#fff').fontWeight(FontWeight.Bold)效果如下:
关键属性:
TextInput({placeholder:'请输入密码'}).width(400).height(40).margin(20).type(InputType.Password).maxLength(9).showPasswordIcon(true)效果如下:
Image同ImageView,用于图片展示,支持png、jpg、bmp、svg和gif类型的图片格式。下面加载图片和网络图片案例:
一般情况下,为了更好的管理项目中的文件,我们会把自定义组件放到一个跟pages目录同级的新的文件夹里,起名叫components或者view。文件夹起名每个人都有自己的喜好,无需强求。但是本文后面以view起名作为存放组件的文件夹
华为鸿蒙官网的Codelabs上大部分示例代码都是以view作为文件夹,所以这里也保持同步
敲“comp”,然后回车即可,DevEco会自动帮我们生成模板:
这是一个装饰器
通过上一篇的学习我们了解到装饰器可以让某个数据具备特殊功能,例如@State可以让数据驱动UI更新
所以@Component这个装饰器就是能让struct这个数据具备组件的功能
因此你会发现默认生成的Index.ets和我们自定义的组件ToDoItem都有这个装饰器
导出:只要在struct前加一个export即可
exportstructToDoItem{....}导入:import语法import{组件名}from'路径'//例import{ToDoItem}from'../view/ToDoItem'我们也可以不写导入的代码,让DevEco自动生成
很显然,有了组件化创建的思维,我们不应再一个文件里面去实现一整个复杂的布局,而是要把复杂的页面进行拆分和细化,像我们的备忘录就应该拆分成顶部显示区、输入任务区、列表区。把不同的区别单独拆分出一个小组件,以便我们去复用、调整布局,在后续维护上也会方便很多。
我们实现一个页面,肯定会存在许多界面与用户交互的逻辑,用户执行操作之后,组件的状态跟随着变化,而ArkUI的状态更新是基于双向绑定实现的:即数据一旦改变,界面跟着变。界面输入内容有变化,数据也跟着变。
我们先来复习一下最常用、最基础的装饰器:
@State用法:
@State变量:类型='初始值'组件传参-父传子当我们把列表拆分成不同的组件的时候,就会存在这样的关系:ToDoMain(整个备忘录控件)相对于ToDoHeader(头部控件)、ToDoInput(输入控件)、ToDoList(任务列表)是父与子的关系,这的时候要使得在父控件更新数据时,子控件同步更新就需要用到另一个修饰器@Prop
我们来简单了解一下@Prop:
@Prop的作用:
数据传递:允许父组件向子组件传递数据。
双向绑定:虽然@Prop主要用于单向数据流,实际使用中我们可以结合其他方式实现双向绑定。
让我们来检验一下这个被别人验证的真理:
例如,本案例中我们有TodoMain和TodoItem,因为TodoMain包含了TodoItem。所以TodoMain是父TodoItem是子。我们就用这两个组件试试父传子
代码步骤:
@ComponentexportstructTodoMain{.......@Statename:string='abc'build(){Column({space:10}){........Row(){......}.onClick(()=>{this.name='修改成功'})ForEach(this.todoList,(item:number)=>{//这里是传参,把父的name传递给了子里的nameTodoItem({name:this.name})})}.width('100%')}}此时会发现,正因为把父的name,也即数据为abc,传递给了子,所以此时TodoItem显示的即为abc,如视频所示:
可以看到点击了并没有重新渲染页面以刷新ui
我们把@Prop修饰在我们的子控件的数据对象试试:
可以看到在子控件添加装饰器@Prop,可以在数据发生变化的时候重新渲染页面。
让我们来与@State对比学习一下:
@State:主要是装饰给组件自己使用的数据,效果:能让成员变量的值改变后,界面也能刷新
@Prop:主要是用在作为子组件时,用来装饰由父传递过来的数据,效果:能让父的数据改变子也能接收到
注意:在ArkTS中,即使父传递的是引用类型的数据,若不加@Prop修饰,一样会导致父的数据改变子里不会变,同学们有兴趣可以自行测试
上面我们讲到,子里的成员变量加@Prop后,即可让父的数据改变,子随之改变,也即父的数据自动同步到子。
但是,目前无法实现子同步到父,也即子里改变了这个父传进来的数据,子里自身能改变,但是父的无法改变。也即Vue框架里的单向数据流
例:在TodoMain里用一个Text显示name的值,并在TodoItem里给Row加点击事件并修改name的值,我们可观察效果
@Statename:string='abc'build(){//省略其他代码...Column({space:10}){//省略其他代码...Text("当前父控件内容:"+this.name).margin({top:20,bottom:20})}//省略其他代码...}让我们来看看效果:
如果我们把子控件ToDoItem中的name换成用@Link修饰,(被@Link修饰的变量不可以初始化,所以直接替换该修饰器会报错,我们需要去掉初始值)那么就会是这样的效果:
可以看到:当我们点击子控件后,父控件内容中的name变量也会跟着改变,并且页面重新渲染,刷新了父控件,每一个子item的内容也跟着改变,这是因为在父控件ToDoMain中,每个子item的Text也是用了name这个成员变量,这就说明@Link是支持###父传子双向同步的。
总结@Prop与@Link相同的和不同点:
相同点:
都是用在子组件,用来接收父传递过来的数据,
都可以实现父改变数据后同步给子
不同点:
初始化值不同:@Prop需要初始化值,相当于给默认值。可以实现,父如果传了就用父的数据,如果没传则用默认值@Link不能初始化,相当于必须要父传递数据了才有数据
同步给父不同:@Prop修饰的数据,子里改变了不会同步给父,@Link修饰的数据,子里改变了会同步给父
我们先仔细回顾一下我们备忘录的交互逻辑:
当我们点击子控件之后,要更新自己的状态,也要更新父控件的状态,输入新的任务的时候还要在子列表中添加新的item,也就是需要父子双向更新状态,很多同学会说那这只需要在子控件的列表数据用@Link修饰就好了,事实是这样的吗,让我们来一起验证一下:
让我们把子控件的ToDoList用@Link修饰,会发现编译器竟然报错了:
这就是目前鸿蒙开发的缺陷缺陷,我们来一下了解一下:
什么叫第一层?
就好比一个数组,数组里全是对象。对于数组而言即为第一层,数组里的每个对象称之为第二层,以此类推
再好比一个嵌套对象。即对象里有个属性又是对象,那么外层的称之为第一层,里面的属性即为第二层,以此类推
所以上述报错里写的item相当于就是数组里的对象,也即第二层,所以报错
出现这个语法限制的根本原因是:目前的鸿蒙开发中,默认情况下无法监听到第二层的改动。而@Link又要实现双向同步,你都无法监听到改动,又如何完成双向同步呢?
所以鸿蒙也给了解决方案:使用@Observed加@ObjectLink来解决。但是,这里不打算讲它。因为这个解决方案其实用起来也很麻烦繁琐,非常不人性化。
那,这里怎么解决上述缺陷呢?首先,因为@Link目前不能用,那咱们就把它换回@Prop先
.....@ComponentexportstructTodoItem{@Propitem:TodoModel.....}可是@Prop又确实无法让父的数据同步改变,该怎么办呢?
既然子里无法改动到父,那就换个思路。让父,自己改!!
整体思路是:让父提供一个修改数据的方法,子里要修改时调用父的方法即可修改!流程图如下:
changeStatus(item:TodoModel,index:number){this.todoList[index]={text:item.text,finished:!item.finished}}解释:
本方法需要传入被点的item以及被点的item的索引
②通过索引的方式改掉数组中这一项,文字不变,但是完成状态取反即可
这时候可能有老铁有疑问:
为什么不直接item=!item.finished还是那个问题:目前不支持监听第二层数据改变,直接改item还是第二层。但数组是第一层,因此你用数组[索引]的方式,就是在改第一层数据,这是能被监听到的
exportstructTodoItem{......onChange:()=>void=()=>{}.....}然后给Select组件加onChange事件,这个事件是当Select发生勾选状态改变就会调用的事件,在里面调用传入的onChange方法
Checkbox().select(this.item.finished).margin({left:20,right:20}).onChange(()=>{this.onChange()})回到TodoMain做方法传递:此时调用TodoItem除了之前要传入的item,现在还要多一个onChange
ForEach(this.todoList,(item:TodoModel,index:number)=>{//此时调用TodoItemTodoItem({item,onChange:()=>{this.changeStatus(item,index)}})})解释参数:item即为当前变动的数据,index即为当前数据对应的下标(都是changeStatus需要用到的内容)
我们来验证一下运行效果:
可以看到我们的思路是正确实现我们所预想的效果
到这里我们就算了实现了我们本次教程的Demo,更多细节可拉取对应demo进行查看。