List是一个列表布局的容器组件,使用List可以轻松地显示可滚动的列表信息。List的子组件只能是ListItem组件或者ListItemGroup,前者表示单个列表项,后者用于列表项的分组展示,比如手机联系人界面中的群组等。接下来我们尝试用List模拟一个手机联系人界面。
首先,我们新建一个工程ListDemo,然后在Index页面代码的最上方,用interface关键字定义一个联系人信息的接口:
接下来,在Index的内部定义一个该接口的数组类型状态变量,用以存储联系人列表的信息:
在aboutToAppear()钩子中,对列表进行赋值:
现在数据已经有了,我们删掉build()中的预生成代码,然后用List组件和ListItem组件把联系人列表的第一项构建出来(ListItem只能有一个根节点,因此我们先需要给它创建一个表示行布局容器的row组件作为根节点):
通过各属性方法调整样式,让界面变得稍微好看一点:
依样画葫芦把剩下的联系人信息也构建出来:
电话本的大概样子是写出来了,小伙伴们应该也发现,这种写法费时费力,我们如果想要调整每一项的样式,就得重复同样的代码多次,而且非常的不灵活,无论我们是想增加或者删除一个联系人,都得调整代码,那么有没有更加好用的方法呢?那就是组件封装+ForEach循环渲染,我们先来说组件封装,组件封装有多种方式,最简单的一种就是结构体内用@Builder装饰器装饰一个成员方法:
在这个方法中,我们把每一个ListItem提取了出来,并直接使用参数contact的name和phone,不需要再自己去填写下标。接着我们改进一下build()方法中的代码:
是不是比之前的简洁多了呢?而且现在我们想调整所有联系人项的样式,也变得很容易,只需要调整contactItem()中的代码即可。让我们试试调整一下文字的颜色和粗细,并设置Row组件的内间距让界面变得好看一点(注:预览器中每一项横线粗细不完全相同是预览器本身的bug,之后我们用模拟器或者真机调试不会出现):
但这依然没有解决我们代码不够灵活的问题,如果我们现在增加了一个联系人,那么还得增加一行代码this.contactItem(this.contacts[6]),同样的,我们如果删除了一个联系人,也得删除一行代码。为了解决这个问题,我们可以使用ForEach()方法进行循环渲染。ForEach()方法接收3个参数,ForEach(arr, itemGenerator, keyGenerator):
其中数组arr和项生成器itemGenerator是必选参数,键生成器keyGenerator是可选参数,但是建议keyGenerator每次都传入并使用固定写法(item,index)=>JSON.stringify(item) + index,原因是不传keyGenerator时,ForEach会默认使用列表的索引作为键值,而这会引发一系列界面更新上的问题,在此不作细说。使用ForEach改进后的代码如下:
除了使用@Builder装饰器定义一个内部的成员方法,我们也可以在外部再定义一个非页面组件(不使用@Entry装饰),然后在Index页面中像使用其他预设组件一样使用该组件,注意给组件传参时需要指明组件成员变量的名称,参数值会覆盖组件成员变量的默认值:
那如果我们想在别的页面中也复用这个组件该怎么办呢?那就得在这个组件的struct之前加上export关键字,表示需要导出给别的文件使用,然后在另一个页面导入后使用。我们给IContact接口和ContactListItem组件前都加上关键字export,新建一个页面Test,在页面代码最上方加上import {IContact, ContactListItem} from './Index',这样我们就能在Test页面代码中使用IContact接口和ContactListItem组件了:
(注:默认的HelloWorld的页面使用的相对布局容器组件RelativeContainer,而HelloWorld文本设置了中点对齐,List未手动设置则是默认的左上角对齐,所以看起来List出现在了HelloWorld文本的上方。)
为了方便管理,通常我们会把非页面组件(又称控件)的代码分离到单独的文件中并放置到专门的文件夹中,这样我们下次再想用的时候就可以很方便地找到它了:
别忘记修改Index页面和Test页面的代码,使得分离出去的IContact和ContactList Item能够正确被导入:
现在,让我们来实现一个联系人选择的功能。在Index页面用CheckBox组件创建一个复选框:orig-sign=0rChfJ4DeuvG8zHMk3ZrTiXvf84%3D)
CheckBox组件的select属性方法设置复选框是否被选中,参数,则只有allSelected改变时会同步到复选框状态,复选框状态改变时不会同步到allSelected)。然后我们想让每一个联系人项都有一个复选框,于是我们修改ContactListItem组件:
现在每一个联系人项都有复选框了,但是它们现在是各自为战的,点击全选复选框并不会把所有联系人项的复选框选上,那么怎么才能让它们关联起来呢?这就是我们所要学习的状态管理。
ArkUI提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。
根据状态变量的影响范围,将所有的装饰器可以大致分为:
- 管理组件内状态的装饰器:组件级别的状态管理,可以观察同一个组件树上(即同一个页面内)组件内或不同组件层级的变量变化。
- 管理应用级状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
从数据的传递形式和同步类型层面看,装饰器也可分为:
- 只读的单向传递;
- 可变更的双向传递。
我们先来学习组件级别的状态管理(同一页面内在同一颗组件树上的不同组件)。首先是单向逐层传递装饰器@Prop,我们把ContactListItem组件中的selected改成用@Prop装饰,并在组件初始化的时候把Index页面的allSelected赋值给selected:
现在点击全选复选框,所有联系人的复选框状态也会被同步改变了。可是全选之后取消任意一个联系人项的复选框,全选复选框并不会被取消,这是因为@Prop装饰器装饰的变量是单向传递的,也就是父组件改变了这个状态会传递给子组件,但是子组件改变这个状态并不会传递给父组件。想达到双向传递的目的,我们需要双向逐层传递装饰器@Link。修改ContactListItem的代码,用@Link替换@Prop(注意,用@Link装饰的状态变量不能赋初始值):
现在点击任何联系人项的复选框,都会把状态同步到全选复选框上了。我们也可以不直接把ContactListItem的selected和allSelected关联,而是通过@Watch装饰器监视allSelected的变化,然后编写响应的响应逻辑。我们给ContactListItem组件新增一个allSelected,用@Link装饰器把它跟Index页面的allSelected链接起来,再用@Watch装饰器来监视它。@Watch需要接收一个成员方法名作为参数,当监控的变量更新时就会调用该成员方法:
现在勾选全选框会选择所有的联系人项,而勾选单个联系人项不会勾选全选框,取消单个联系人项会把全选框也取消了。虽然跟正常的功能逻辑相比还有点瑕疵,但是作为教学演示已经达到目的了,剩下的就交给小伙伴们自己去完善: