1# 服务卡片开发指导 2 3 4## 卡片概述 5 6服务卡片(以下简称“卡片”)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。 7 8卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面、发送消息等基础的交互功能。 9 10卡片的基本概念: 11 12- 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。 13 14- 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。 15 16- 卡片提供方:提供卡片显示内容原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。 17 18 19## 运作机制 20 21卡片框架的运作机制如图1所示。 22 23 **图1** 卡片框架运作机制(FA模型) 24 25 26卡片使用方包含以下模块: 27 28- 卡片使用:包含卡片的创建、删除、请求更新等操作。 29 30- 通信适配层:由OpenHarmony SDK提供,负责与卡片管理服务通信,用于将卡片的相关操作到卡片管理服务。 31 32卡片管理服务包含以下模块: 33 34- 周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。 35 36- 卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。 37 38- 卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。 39 40- 卡片使用方对象管理:对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。 41 42- 通信适配层:负责与卡片使用方和提供方进行RPC通信。 43 44卡片提供方包含以下模块: 45 46- 卡片服务:由卡片提供方开发者实现,开发者实现生命周期处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。 47 48- 卡片提供方实例管理模块:由卡片提供方开发者实现,负责对卡片管理服务分配的卡片实例进行持久化管理。 49 50- 通信适配层:由OpenHarmony SDK提供,负责与卡片管理服务通信,用于将卡片的更新数据主动推送到卡片管理服务。 51 52> **说明:** 53> 实际开发时只需要作为卡片提供方进行卡片内容的开发,卡片使用方和卡片管理服务由系统自动处理。 54 55 56## 接口说明 57 58FormAbility生命周期接口如下: 59 60| 接口名 | 描述 | 61| -------- | -------- | 62| onCreate(want: Want): formBindingData.FormBindingData | 卡片提供方接收创建卡片的通知接口。 | 63| onCastToNormal(formId: string): void | 卡片提供方接收临时卡片转常态卡片的通知接口 | 64| onUpdate(formId: string): void | 卡片提供方接收更新卡片的通知接口。 | 65| onVisibilityChange(newStatus: { [key: string]: number }): void | 卡片提供方接收修改可见性的通知接口。 | 66| onEvent(formId: string, message: string): void | 卡片提供方接收处理卡片事件的通知接口。 | 67| onDestroy(formId: string): void | 卡片提供方接收销毁卡片的通知接口。 | 68| onAcquireFormState?(want: Want): formInfo.FormState | 卡片提供方接收查询卡片状态的通知接口。 | 69| onShare?(formId: string): {[key: string]: any} | 卡片提供方接收卡片分享的通知接口。 | 70 71FormProvider类有如下API接口,具体的API介绍详见[接口文档](../reference/apis/js-apis-app-form-formProvider.md)。 72 73 74| 接口名 | 描述 | 75| -------- | -------- | 76| setFormNextRefreshTime(formId: string, minute: number, callback: AsyncCallback<void>): void; | 设置指定卡片的下一次更新时间。 | 77| setFormNextRefreshTime(formId: string, minute: number): Promise<void>; | 设置指定卡片的下一次更新时间,以promise方式返回。 | 78| updateForm(formId: string, formBindingData: FormBindingData, callback: AsyncCallback<void>): void; | 更新指定的卡片。 | 79| updateForm(formId: string, formBindingData: FormBindingData): Promise<void>; | 更新指定的卡片,以promise方式返回。 | 80 81 82formBindingData类有如下API接口,具体的API介绍详见[接口文档](../reference/apis/js-apis-app-form-formBindingData.md)。 83 84 85| 接口名 | 描述 | 86| -------- | -------- | 87| createFormBindingData(obj?: Object \| string): FormBindingData | 创建一个FormBindingData对象。 | 88 89 90## 开发步骤 91 92FA卡片开发,即基于[FA模型](fa-model-development-overview.md)的卡片提供方开发,主要涉及如下关键步骤: 93 94- [实现卡片生命周期接口](#实现卡片生命周期接口):开发FormAbility生命周期回调函数。 95 96- [配置卡片配置文件](#配置卡片配置文件):配置应用配置文件config.json。 97 98- [卡片信息的持久化](#卡片信息的持久化):对卡片信息进行持久化管理。 99 100- [卡片数据交互](#卡片数据交互):通过updateForm()更新卡片显示的信息。 101 102- [开发卡片页面](#开发卡片页面):使用HML+CSS+JSON开发JS卡片页面。 103 104- [开发卡片事件](#开发卡片事件):为卡片添加router事件和message事件。 105 106 107### 实现卡片生命周期接口 108 109创建FA模型的卡片,需实现卡片的生命周期接口。先参考[IDE开发服务卡片指南](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-development-service-widget-0000001263280425)生成服务卡片模板。 110 1111. 在form.ts中,导入相关模块 112 113 ```ts 114 import formBindingData from '@ohos.app.form.formBindingData'; 115 import formInfo from '@ohos.app.form.formInfo'; 116 import formProvider from '@ohos.app.form.formProvider'; 117 import dataPreferences from '@ohos.data.preferences'; 118 import Want from '@ohos.app.ability.Want'; 119 ``` 120 1212. 在form.ts中,实现卡片生命周期接口 122 123 ```ts 124 class lifeCycle { 125 onCreate: (want: Want) => formBindingData.FormBindingData = (want) => ({ data: '' }) 126 onCastToNormal: (formId: string) => void = (formId) => {} 127 onUpdate: (formId: string) => void = (formId) => {} 128 onVisibilityChange: (newStatus: Record<string, number>) => void = (newStatus) => { 129 let obj: Record<string, number> = { 130 'test': 1 131 }; 132 return obj; 133 } 134 onEvent: (formId: string, message: string) => void = (formId, message) => {} 135 onDestroy: (formId: string) => void = (formId) => {} 136 onAcquireFormState?: (want: Want) => formInfo.FormState = (want) => (0) 137 onShare?: (formId: string) => Record<string, number | string | boolean | object | undefined | null> = (formId) => { 138 let obj: Record<string, number> = { 139 'test': 1 140 }; 141 return obj; 142 } 143 } 144 145 let obj: lifeCycle = { 146 onCreate(want: Want) { 147 console.info('FormAbility onCreate'); 148 // 使用方创建卡片时触发,提供方需要返回卡片数据绑定类 149 let obj: Record<string, string> = { 150 "title": "titleOnCreate", 151 "detail": "detailOnCreate" 152 }; 153 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 154 return formData; 155 }, 156 onCastToNormal(formId: string) { 157 // 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理 158 console.info('FormAbility onCastToNormal'); 159 }, 160 onUpdate(formId: string) { 161 // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新 162 console.info('FormAbility onUpdate'); 163 let obj: Record<string, string> = { 164 "title": "titleOnUpdate", 165 "detail": "detailOnUpdate" 166 }; 167 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 168 formProvider.updateForm(formId, formData).catch((error: Error) => { 169 console.info('FormAbility updateForm, error:' + JSON.stringify(error)); 170 }); 171 }, 172 onVisibilityChange(newStatus: Record<string, number>) { 173 // 使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效 174 console.info('FormAbility onVisibilityChange'); 175 }, 176 onEvent(formId: string, message: string) { 177 // 若卡片支持触发事件,则需要重写该方法并实现对事件的触发 178 console.info('FormAbility onEvent'); 179 }, 180 onDestroy(formId: string) { 181 // 删除卡片实例数据 182 console.info('FormAbility onDestroy'); 183 }, 184 onAcquireFormState(want: Want) { 185 console.info('FormAbility onAcquireFormState'); 186 return formInfo.FormState.READY; 187 }, 188 } 189 190 export default obj; 191``` 192 193> **说明:** 194> FormAbility不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务。 195 196### 配置卡片配置文件 197 198卡片需要在应用配置文件config.json中进行配置。 199 200- js模块,用于对应卡片的js相关资源,内部字段结构说明: 201 | 属性名称 | 含义 | 数据类型 | 是否可缺省 | 202 | -------- | -------- | -------- | -------- | 203 | name | 表示JS Component的名字。该标签不可缺省,默认值为default。 | 字符串 | 否 | 204 | pages | 表示JS Component的页面用于列举JS Component中每个页面的路由信息[页面路径+页面名称]。该标签不可缺省,取值为数组,数组第一个元素代表JS FA首页。 | 数组 | 否 | 205 | window | 用于定义与显示窗口相关的配置。 | 对象 | 可缺省 | 206 | type | 表示JS应用的类型。取值范围如下:<br/>normal:标识该JS Component为应用实例。<br/>form:标识该JS Component为卡片实例。 | 字符串 | 可缺省,缺省值为“normal” | 207 | mode | 定义JS组件的开发模式。 | 对象 | 可缺省,缺省值为空 | 208 209 配置示例如下: 210 211 212 ```json 213 "js": [{ 214 "name": "widget", 215 "pages": ["pages/index/index"], 216 "window": { 217 "designWidth": 720, 218 "autoDesignWidth": true 219 }, 220 "type": "form" 221 }] 222 ``` 223 224- abilities模块,用于对应卡片的FormAbility,内部字段结构说明: 225 | 属性名称 | 含义 | 数据类型 | 是否可缺省 | 226 | -------- | -------- | -------- | -------- | 227 | name | 表示卡片的类名。字符串最大长度为127字节。 | 字符串 | 否 | 228 | description | 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。 | 字符串 | 可缺省,缺省为空。 | 229 | isDefault | 表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。<br/>true:默认卡片。<br/>false:非默认卡片。 | 布尔值 | 否 | 230 | type | 表示卡片的类型。取值范围如下:<br/>JS:JS卡片。 | 字符串 | 否 | 231 | colorMode | 表示卡片的主题样式,取值范围如下:<br/>auto:自适应。<br/>dark:深色主题。<br/>light:浅色主题。 | 字符串 | 可缺省,缺省值为“auto”。 | 232 | supportDimensions | 表示卡片支持的外观规格,取值范围:<br/>1 \* 2:表示1行2列的二宫格。<br/>2 \* 2:表示2行2列的四宫格。<br/>2 \* 4:表示2行4列的八宫格。<br/>4 \* 4:表示4行4列的十六宫格。 | 字符串数组 | 否 | 233 | defaultDimension | 表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。 | 字符串 | 否 | 234 | updateEnabled | 表示卡片是否支持周期性刷新,取值范围:<br/>true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新。<br/>false:表示不支持周期性刷新。 | 布尔类型 | 否 | 235 | scheduledUpdateTime | 表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。<br/>updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。 | 字符串 | 可缺省,缺省值为“0:0”。 | 236 | updateDuration | 表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。<br/>当取值为0时,表示该参数不生效。<br/>当取值为正整数N时,表示刷新周期为30\*N分钟。<br/>updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。 | 数值 | 可缺省,缺省值为“0”。 | 237 | formConfigAbility | 表示卡片的配置跳转链接,采用URI格式。 | 字符串 | 可缺省,缺省值为空。 | 238 | formVisibleNotify | 标识是否允许卡片使用卡片可见性通知。 | 字符串 | 可缺省,缺省值为空。 | 239 | jsComponentName | 表示JS卡片的Component名称。字符串最大长度为127字节。 | 字符串 | 否 | 240 | metaData | 表示卡片的自定义信息,包含customizeData数组标签。 | 对象 | 可缺省,缺省值为空。 | 241 | customizeData | 表示自定义的卡片信息。 | 对象数组 | 可缺省,缺省值为空。 | 242 243 配置示例如下: 244 245 246 ```json 247 "abilities": [{ 248 "name": "FormAbility", 249 "description": "This is a FormAbility", 250 "formsEnabled": true, 251 "icon": "$media:icon", 252 "label": "$string:form_FormAbility_label", 253 "srcPath": "FormAbility", 254 "type": "service", 255 "srcLanguage": "ets", 256 "formsEnabled": true, 257 "formConfigAbility": "ability://com.example.entry.EntryAbility", 258 "forms": [{ 259 "colorMode": "auto", 260 "defaultDimension": "2*2", 261 "description": "This is a service widget.", 262 "formVisibleNotify": true, 263 "isDefault": true, 264 "jsComponentName": "widget", 265 "name": "widget", 266 "scheduledUpdateTime": "10:30", 267 "supportDimensions": ["2*2"], 268 "type": "JS", 269 "updateEnabled": true 270 }] 271 }] 272 ``` 273 274 275### 卡片信息的持久化 276 277因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息,且卡片管理服务支持对卡片进行多实例管理,卡片ID对应实例ID,因此若卡片提供方支持对卡片数据进行配置,则需要对卡片的业务数据按照卡片ID进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。 278 279 280```ts 281const DATA_STORAGE_PATH: string = "/data/storage/el2/base/haps/form_store"; 282let storeFormInfo = async (formId: string, formName: string, tempFlag: boolean, context: Context) => { 283 // 此处仅对卡片ID:formId,卡片名:formName和是否为临时卡片:tempFlag进行了持久化 284 let formInfo: Record<string, string | number | boolean> = { 285 "formName": formName, 286 "tempFlag": tempFlag, 287 "updateCount": 0 288 }; 289 try { 290 const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH); 291 // put form info 292 await storage.put(formId, JSON.stringify(formInfo)); 293 console.info(`storeFormInfo, put form info successfully, formId: ${formId}`); 294 await storage.flush(); 295 } catch (err) { 296 console.error(`failed to storeFormInfo, err: ${JSON.stringify(err as Error)}`); 297 } 298} 299 300... 301 onCreate(want: Want) { 302 console.info('FormAbility onCreate'); 303 304 if (want.parameters) { 305 let formId = String(want.parameters["ohos.extra.param.key.form_identity"]); 306 let formName = String(want.parameters["ohos.extra.param.key.form_name"]); 307 let tempFlag = Boolean(want.parameters["ohos.extra.param.key.form_temporary"]); 308 // 将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用 309 // 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例 310 storeFormInfo(formId, formName, tempFlag, this.context); 311 } 312 313 let obj: Record<string, string> = { 314 "title": "titleOnCreate", 315 "detail": "detailOnCreate" 316 }; 317 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 318 return formData; 319 } 320... 321``` 322 323且需要适配onDestroy卡片删除通知接口,在其中实现卡片实例数据的删除。 324 325 326```ts 327const DATA_STORAGE_PATH: string = "/data/storage/el2/base/haps/form_store"; 328let deleteFormInfo = async (formId: string, context: Context) => { 329 try { 330 const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH); 331 // del form info 332 await storage.delete(formId); 333 console.info(`deleteFormInfo, del form info successfully, formId: ${formId}`); 334 await storage.flush(); 335 } catch (err) { 336 console.error(`failed to deleteFormInfo, err: ${JSON.stringify(err)}`); 337 } 338} 339 340... 341 onDestroy(formId: string) { 342 console.info('FormAbility onDestroy'); 343 // 删除之前持久化的卡片实例数据 344 // 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例 345 deleteFormInfo(formId, this.context); 346 } 347... 348``` 349 350具体的持久化方法可以参考[数据管理开发指导](../database/app-data-persistence-overview.md)。 351 352需要注意的是,卡片使用方在请求卡片时传递给提供方应用的Want数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片: 353 354- 常态卡片:卡片使用方会持久化的卡片。如添加到桌面的卡片。 355 356- 临时卡片:卡片使用方不会持久化的卡片。如上划卡片应用时显示的卡片。 357 358临时卡片转常态卡片:上划卡片应用后,此时会显示的卡片为临时卡片;点击卡片上的“图钉”按钮后添加到桌面,此时卡片转为常态卡片。 359 360由于临时卡片的数据具有非持久化的特殊性,某些场景例如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。 361 362 363### 卡片数据交互 364 365当卡片应用需要更新数据时(如触发了定时更新或定点更新),卡片应用获取最新数据,并调用updateForm()接口更新主动触发卡片的更新。 366 367 368```ts 369onUpdate(formId: string) { 370 // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新 371 console.info('FormAbility onUpdate'); 372 let obj: Record<string, string> = { 373 "title": "titleOnUpdate", 374 "detail": "detailOnUpdate" 375 }; 376 let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj); 377 // 调用updateForm接口去更新对应的卡片,仅更新入参中携带的数据信息,其他信息保持不变 378 formProvider.updateForm(formId, formData).catch((error: Error) => { 379 console.info('FormAbility updateForm, error:' + JSON.stringify(error)); 380 }); 381} 382``` 383 384 385### 开发卡片页面 386 387开发者可以使用类Web范式(HML+CSS+JSON)开发JS卡片页面。生成如下卡片页面,可以这样配置卡片页面文件: 388 389 390 391> **说明:** 392> FA模型当前仅支持JS扩展的类Web开发范式来实现卡片的UI。 393 394- HML:使用类Web范式的组件描述卡片的页面信息。 395 396 ```html 397 <div class="container"> 398 <stack> 399 <div class="container-img"> 400 <image src="/common/widget.png" class="bg-img"></image> 401 </div> 402 <div class="container-inner"> 403 <text class="title">{{title}}</text> 404 <text class="detail_text" onclick="routerEvent">{{detail}}</text> 405 </div> 406 </stack> 407 </div> 408 ``` 409 410- CSS:HML中类Web范式组件的样式信息。 411 412 ```css 413 .container { 414 flex-direction: column; 415 justify-content: center; 416 align-items: center; 417 } 418 419 .bg-img { 420 flex-shrink: 0; 421 height: 100%; 422 } 423 424 .container-inner { 425 flex-direction: column; 426 justify-content: flex-end; 427 align-items: flex-start; 428 height: 100%; 429 width: 100%; 430 padding: 12px; 431 } 432 433 .title { 434 font-size: 19px; 435 font-weight: bold; 436 color: white; 437 text-overflow: ellipsis; 438 max-lines: 1; 439 } 440 441 .detail_text { 442 font-size: 16px; 443 color: white; 444 opacity: 0.66; 445 text-overflow: ellipsis; 446 max-lines: 1; 447 margin-top: 6px; 448 } 449 ``` 450 451- JSON:卡片页面中的数据和事件交互。 452 453 ```json 454 { 455 "data": { 456 "title": "TitleDefault", 457 "detail": "TextDefault" 458 }, 459 "actions": { 460 "routerEvent": { 461 "action": "router", 462 "abilityName": "com.example.entry.EntryAbility", 463 "params": { 464 "message": "add detail" 465 } 466 } 467 } 468 } 469 ``` 470 471 472### 开发卡片事件 473 474卡片支持为组件设置交互事件(action),包括router事件和message事件,其中router事件用于Ability跳转,message事件用于卡片开发人员自定义点击事件。关键步骤说明如下: 475 4761. 在hml中为组件设置onclick属性,其值对应到json文件的actions字段中。 477 4782. 如何设置router事件: 479 - action属性值为"router"; 480 - abilityName为跳转目标的Ability名(支持跳转FA模型的PageAbility组件和Stage模型的UIAbility组件),如目前DevEco创建的FA模型的UIAbility默认名为com.example.entry.EntryAbility; 481 - params为传递给跳转目标Ability的自定义参数,可以按需填写。其值可以在目标Ability启动时的want中的parameters里获取。如FA模型EntryAbility的onCreate生命周期里可以通过featureAbility.getWant()获取到want,然后在其parameters字段下获取到配置的参数; 482 4833. 如何设置message事件: 484 - action属性值为"message"; 485 - params为message事件的用户自定义参数,可以按需填写。其值可以在卡片生命周期函数onEvent中的message里获取; 486 487示例如下: 488 489- hml文件 490 491 ```html 492 <div class="container"> 493 <stack> 494 <div class="container-img"> 495 <image src="/common/widget.png" class="bg-img"></image> 496 </div> 497 <div class="container-inner"> 498 <text class="title" onclick="routerEvent">{{title}}</text> 499 <text class="detail_text" onclick="messageEvent">{{detail}}</text> 500 </div> 501 </stack> 502 </div> 503 ``` 504 505- css文件 506 507 ```css 508 .container { 509 flex-direction: column; 510 justify-content: center; 511 align-items: center; 512 } 513 514 .bg-img { 515 flex-shrink: 0; 516 height: 100%; 517 } 518 519 .container-inner { 520 flex-direction: column; 521 justify-content: flex-end; 522 align-items: flex-start; 523 height: 100%; 524 width: 100%; 525 padding: 12px; 526 } 527 528 .title { 529 font-size: 19px; 530 font-weight: bold; 531 color: white; 532 text-overflow: ellipsis; 533 max-lines: 1; 534 } 535 536 .detail_text { 537 font-size: 16px; 538 color: white; 539 opacity: 0.66; 540 text-overflow: ellipsis; 541 max-lines: 1; 542 margin-top: 6px; 543 } 544 ``` 545 546- json文件 547 548 ```json 549 { 550 "data": { 551 "title": "TitleDefault", 552 "detail": "TextDefault" 553 }, 554 "actions": { 555 "routerEvent": { 556 "action": "router", 557 "abilityName": "com.example.entry.EntryAbility", 558 "params": { 559 "message": "add detail" 560 } 561 }, 562 "messageEvent": { 563 "action": "message", 564 "params": { 565 "message": "add detail" 566 } 567 } 568 } 569 } 570 ``` 571