# MVVMæ¨¡å¼ å½“å¼€å‘者了解了状æ€ç®¡ç†çš„æ¦‚念之åŽï¼Œè·ƒè·ƒæ¬²è¯•去开å‘一款自己的应用,倘若开å‘è€…åœ¨åº”ç”¨å¼€å‘æ—¶ä¸æ³¨æ„设计自己的项目结构,éšç€é¡¹ç›®è¶Šæ¥è¶Šåºžå¤§ï¼Œçжæ€å˜é‡è®¾è®¡çš„è¶Šæ¥è¶Šå¤šï¼Œç»„件与组件之间的关系å˜å¾—è¶Šæ¥è¶Šæ¨¡ç³Šï¼Œå½“å¼€å‘一个新需求时,牵一å‘而动全身,需求开å‘å’Œç»´æŠ¤æˆæœ¬ä¹Ÿä¼šæˆå€å¢žåŠ ï¼Œä¸ºæ¤ï¼Œæœ¬æ–‡æ—¨åœ¨ä»‹ç»MVVM模å¼ä»¥åŠArkUIçš„UI开呿¨¡å¼ä¸ŽMVVM的关系,指引开å‘者如何去设计自己的项目结构,从而在产å“è¿ä»£å’Œå‡çº§æ—¶ï¼Œèƒ½æ›´å®¹æ˜“的去开å‘和维护。 本文档涵盖了大多数状æ€ç®¡ç†V1装饰器,所以在阅读本文当å‰ï¼Œå»ºè®®å¼€å‘者对状æ€ç®¡ç†V1有一定的了解。建议æå‰é˜…读:[状æ€ç®¡ç†æ¦‚è¿°](./arkts-state-management-overview.md)和状æ€ç®¡ç†V1装饰器相关文档。 ## MVVM模å¼ä»‹ç» ### 概念 在应用开å‘ä¸ï¼ŒUI的更新需è¦éšç€æ•°æ®çжæ€çš„å˜åŒ–è¿›è¡Œå®žæ—¶åŒæ¥ï¼Œè€Œè¿™ç§åŒæ¥å¾€å¾€å†³å®šäº†åº”用程åºçš„æ€§èƒ½å’Œç”¨æˆ·ä½“验。为了解决数æ®ä¸ŽUIåŒæ¥çš„夿‚性,ArkUI采用了 Model-View-ViewModel(MVVM)架构模å¼ã€‚MVVM 将应用分为Modelã€Viewå’ŒViewModelä¸‰ä¸ªæ ¸å¿ƒéƒ¨åˆ†ï¼Œå®žçŽ°æ•°æ®ã€è§†å›¾ä¸Žé€»è¾‘çš„åˆ†ç¦»ã€‚é€šè¿‡è¿™ç§æ¨¡å¼ï¼ŒUIå¯ä»¥éšç€çжæ€çš„å˜åŒ–è‡ªåŠ¨æ›´æ–°ï¼Œæ— éœ€æ‰‹åŠ¨å¤„ç†ï¼Œä»Žè€Œæ›´åŠ é«˜æ•ˆåœ°ç®¡ç†æ•°æ®å’Œè§†å›¾çš„绑定与更新。 - Model:负责å˜å‚¨å’Œç®¡ç†åº”用的数æ®ä»¥åŠä¸šåŠ¡é€»è¾‘ï¼Œä¸ç›´æŽ¥ä¸Žç”¨æˆ·ç•Œé¢äº¤äº’。通常从åŽç«¯æŽ¥å£èŽ·å–æ•°æ®ï¼Œæ˜¯åº”用程åºçš„æ•°æ®åŸºç¡€ï¼Œç¡®ä¿æ•°æ®çš„一致性和完整性。 - View:负责用户界é¢å±•示数æ®å¹¶ä¸Žç”¨æˆ·äº¤äº’,ä¸åŒ…å«ä»»ä½•业务逻辑。它通过绑定ViewModel层æä¾›çš„æ•°æ®æ¥åŠ¨æ€æ›´æ–°UI。 - ViewModel:负责管ç†UI状æ€å’Œäº¤äº’逻辑。作为连接Modelå’ŒView的桥æ¢ï¼ŒViewModel监控Modelæ•°æ®çš„å˜åŒ–,通知Viewæ›´æ–°UIï¼ŒåŒæ—¶å¤„ç†ç”¨æˆ·äº¤äº’事件并转æ¢ä¸ºæ•°æ®æ“作。 ArkUIçš„UI开呿¨¡å¼å°±å±žäºŽMVVM模å¼ï¼Œé€šè¿‡å¯¹MVVM概念的基本介ç»ï¼Œå¼€å‘者大致能猜到状æ€ç®¡ç†èƒ½åœ¨MVVMä¸èµ·ä»€ä¹ˆæ ·çš„作用,状æ€ç®¡ç†æ—¨åœ¨æ•°æ®é©±åŠ¨æ›´æ–°ï¼Œè®©å¼€å‘者åªç”¨å…³æ³¨é¡µé¢è®¾è®¡ï¼Œè€Œä¸åŽ»å…³æ³¨æ•´ä¸ªUI的刷新逻辑,数æ®çš„ç»´æŠ¤ä¹Ÿæ— éœ€å¼€å‘者进行感知,由状æ€å˜é‡è‡ªåŠ¨æ›´æ–°å®Œæˆï¼Œè€Œè¿™å°±æ˜¯å±žäºŽViewModelå±‚æ‰€éœ€è¦æ”¯æŒçš„å†…å®¹ï¼Œå› æ¤å¼€å‘者使用MVVM模å¼å¼€å‘自己的应用是最çœå¿ƒçœåŠ›çš„ã€‚ ### ArkUI开呿¨¡å¼å›¾ ArkUIçš„UIå¼€å‘开呿¨¡å¼å³æ˜¯MVVM模å¼ï¼Œè€Œçжæ€å˜é‡åœ¨MVVM模å¼ä¸æ‰®æ¼”ç€ViewModel的角色,å‘上刷新UI,å‘下更新数æ®ï¼Œæ•´ä½“框架如下图:  ### 分层说明 **View层** * 页é¢ç»„件:所有应用基本都是按照页é¢è¿›è¡Œåˆ†ç±»çš„,比如登录页,列表页,编辑页,帮助页,版æƒé¡µç‰ã€‚æ¯ä¸ªé¡µå¯¹åº”需è¦çš„æ•°æ®å¯èƒ½æ˜¯å®Œå…¨ä¸ä¸€æ ·çš„,也å¯èƒ½å¤šä¸ªé¡µé¢éœ€è¦çš„æ•°æ®æ˜¯åŒä¸€å¥—。 * 业务组件:本身具备本APP部分业务能力的功能组件,典型的就是这个业务组件å¯èƒ½å…³è”了本项目的ViewModelä¸çš„æ•°æ®ï¼Œä¸å¯ä»¥è¢«å…±äº«ç»™å…¶ä»–项目使用。 * 通用组件:åƒå†…ç½®ç»„ä»¶ä¸€æ ·ï¼Œè¿™ç±»ç»„ä»¶ä¸ä¼šå…³è”本APPä¸ViewModel的数æ®ï¼Œè¿™äº›ç»„ä»¶å¯å®žçŽ°è·¨è¶Šå¤šä¸ªé¡¹ç›®è¿›è¡Œå…±äº«ï¼Œæ¥å®Œæˆæ¯”较通用的功能。 **ViewModel层** * 页颿•°æ®ï¼šæŒ‰ç…§é¡µé¢ç»„织的数æ®ï¼Œç”¨æˆ·æ‰“开页颿—¶ï¼Œå¯èƒ½æŸäº›é¡µé¢å¹¶ä¸ä¼šåˆ‡æ¢åˆ°ï¼Œå› æ¤ï¼Œè¿™ä¸ªé¡µé¢æ•°æ®æœ€å¥½è®¾è®¡æˆæ‡’åŠ è½½çš„æ¨¡å¼ã€‚ > ViewModel层数æ®å’ŒModel层数æ®çš„区别: > > Modelå±‚æ•°æ®æ˜¯æŒ‰ç…§æ•´ä¸ªå·¥ç¨‹ï¼Œé¡¹ç›®æ¥ç»„织数æ®ï¼Œæ˜¯ä¸€å¥—å®Œæˆæœ¬APP的业务数æ®ã€‚ > > ViewModel层数æ®ï¼Œæ˜¯æä¾›æŸä¸ªé¡µé¢ä¸Šä½¿ç”¨çš„æ•°æ®ï¼Œå®ƒå¯èƒ½æ˜¯æ•´ä¸ªAPP的业务数æ®çš„一部分。å¦å¤–ViewModel层还å¯ä»¥é™„åŠ å¯¹åº”Pageçš„è¾…åŠ©é¡µé¢æ˜¾ç¤ºæ•°æ®ï¼Œè¿™éƒ¨åˆ†æ•°æ®å¯èƒ½ä¸Žæœ¬APPçš„ä¸šåŠ¡å®Œå…¨æ— å…³ï¼Œä»…ä»…æ˜¯ä¸ºé¡µé¢å±•示æä¾›ä¾¿åˆ©çš„辅助数æ®ã€‚ **Model层** Modelå±‚æ˜¯åº”ç”¨çš„åŽŸå§‹æ•°æ®æä¾›è€…ï¼Œè¿™ä¸€å±‚åœ¨UIæ¥çœ‹ï¼Œæœ‰ä¸¤ç§æ¨¡å¼ * 本地实现:通过纯NativeC++实现 * 远端实现:通过IO端å£ï¼ˆRestFul)实现 > 注æ„: > > 采用本地实现时,系统的对数æ®åŠ å·¥å’Œå¤„ç†ï¼ŒåŸºæœ¬ä¸Šä¸€å®šä¼šå˜åœ¨éžUIçº¿ç¨‹æ¨¡åž‹ï¼Œè¿™ä¸ªæ—¶å€™ï¼Œè¢«åŠ å·¥çš„æ•°æ®å˜æ›´å¯èƒ½éœ€è¦å³æ—¶é€šçŸ¥ViewModel层,æ¥å¼•èµ·æ•°æ®çš„å˜åŒ–,从而引起UI的相应更新。这个时候,自动线程转æ¢å°±ä¼šå˜å¾—éžå¸¸é‡è¦ã€‚常规下,ViewModel层,View层,都åªèƒ½åœ¨UI线程下执行,æ‰èƒ½æ£å¸¸å·¥ä½œã€‚å› æ¤éœ€è¦ä¸€ç§æœºåˆ¶ï¼Œå½“需è¦é€šçŸ¥UI更新时,需è¦è‡ªåŠ¨å®Œæˆçº¿ç¨‹åˆ‡æ¢ã€‚ ### æž¶æž„æ ¸å¿ƒåŽŸåˆ™ **ä¸å¯è·¨å±‚访问** * View层ä¸å¯ä»¥ç›´æŽ¥è°ƒç”¨Model层的数æ®ï¼Œåªèƒ½é€šè¿‡ViewModelæä¾›çš„æ–¹æ³•进行调用。 * Model层数æ®ï¼Œä¸å¯ä»¥ç›´æŽ¥æ“作UI,Model层åªèƒ½é€šçŸ¥ViewModelå±‚æ•°æ®æœ‰æ›´æ–°ï¼Œç”±ViewModel层更新对应的数æ®ã€‚ **下层ä¸å¯è®¿é—®ä¸Šå±‚æ•°æ®** 下层的数æ®é€šè¿‡é€šçŸ¥æ¨¡å¼æ›´æ–°ä¸Šå±‚æ•°æ®ã€‚在业务逻辑ä¸ï¼Œä¸‹å±‚ä¸å¯ç›´æŽ¥å†™ä»£ç 去获å–上层数æ®ã€‚如ViewModel层的逻辑处ç†ï¼Œä¸èƒ½åŽ»ä¾èµ–View层界é¢ä¸Šçš„æŸä¸ªå€¼ã€‚ **éžçˆ¶å组件间ä¸å¯ç›´æŽ¥è®¿é—®** 这是针对Viewå±‚è®¾è®¡çš„æ ¸å¿ƒåŽŸåˆ™ï¼Œä¸€ä¸ªç»„ä»¶åº”è¯¥å…·å¤‡è¿™æ ·çš„é€»è¾‘ï¼š * ç¦æ¢ç›´æŽ¥è®¿é—®çˆ¶ç»„件(使用事件或是订阅能力) * ç¦æ¢ç›´æŽ¥è®¿é—®å…„å¼Ÿç»„ä»¶èƒ½åŠ›ã€‚è¿™æ˜¯å› ä¸ºç»„ä»¶åº”è¯¥ä»…èƒ½è®¿é—®è‡ªå·±çœ‹çš„è§çš„åèŠ‚ç‚¹ï¼ˆé€šè¿‡ä¼ å‚)和父节点(通过事件或通知),以æ¤å®Œæˆç»„件之间的解耦。 å¯¹äºŽä¸€ä¸ªç»„ä»¶ï¼Œè¿™æ ·è®¾è®¡çš„åŽŸå› æ˜¯ï¼š * 组件自己使用了哪些åç»„ä»¶æ˜¯æ˜Žç¡®çš„ï¼Œå› æ¤å¯ä»¥è®¿é—®ã€‚ * ç»„ä»¶è¢«æ”¾ç½®äºŽå“ªä¸ªçˆ¶èŠ‚ç‚¹ä¸‹æ˜¯æœªçŸ¥çš„ï¼Œå› æ¤ç»„件想访问父节点,就åªèƒ½é€šè¿‡é€šçŸ¥æˆ–者事件能力完æˆã€‚ * 组件ä¸å¯èƒ½çŸ¥é“自己的兄弟节点是è°ï¼Œå› æ¤ç»„ä»¶ä¸å¯ä»¥æ“纵兄弟节点。 ## 备忘录开å‘实战 本节通过备忘录应用的开å‘,让开å‘者了解如何通过ArkUIæ¡†æž¶è®¾è®¡è‡ªå·±çš„åº”ç”¨ï¼Œæœ¬èŠ‚æœªè®¾è®¡ä»£ç æž¶æž„直接进行功能开å‘ï¼Œå³æ ¹æ®éœ€æ±‚åšå³æ—¶å¼€å‘,ä¸è€ƒè™‘åŽç»ç»´æŠ¤ï¼ŒåŒæ—¶å‘å¼€å‘者介ç»åŠŸèƒ½å¼€å‘æ‰€éœ€çš„装饰器。 ### @State状æ€å˜é‡ * @State装饰器作为最常用的装饰器,用æ¥å®šä¹‰çжæ€å˜é‡ï¼Œä¸€èˆ¬ä½œä¸ºçˆ¶ç»„ä»¶çš„æ•°æ®æºï¼Œå½“å¼€å‘者点击时,通过触å‘状æ€å˜é‡çš„æ›´æ–°ä»Žè€Œåˆ·æ–°UI,去掉@State则ä¸å†æ”¯æŒåˆ·æ–°UI。 ```typescript @Entry @Component struct Index { @State isFinished: boolean = false; build() { Column() { Row() { Text('全部待办') .fontSize(30) .fontWeight(FontWeight.Bold) } .width('100%') .margin({top: 10, bottom: 10}) // 待办事项 Row({space: 15}) { if (this.isFinished) { // æ¤å¤„'app.media.finished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.finished')) .width(28) .height(28) } else { // æ¤å¤„'app.media.unfinished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.unfinished')) .width(28) .height(28) } Text('å¦ä¹ 高数') .fontSize(24) .fontWeight(450) .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) } .height('40%') .width('100%') .border({width: 5}) .padding({left: 15}) .onClick(() => { this.isFinished = !this.isFinished; }) } .height('100%') .width('100%') .margin({top: 5, bottom: 5}) .backgroundColor('#90f1f3f5') } } ``` 效果图:  ### @Propã€@Link的作用 上述示例ä¸ï¼Œæ‰€æœ‰çš„代ç 都写在了@Entry组件ä¸ï¼Œéšç€éœ€è¦æ¸²æŸ“的组件越æ¥è¶Šå¤šï¼Œ@Entry组件必然需è¦è¿›è¡Œæ‹†åˆ†ï¼Œä¸ºæ¤æ‹†åˆ†å‡ºçš„å组件就需è¦ä½¿ç”¨@Propå’Œ@Link装饰器: * @Prop是父åé—´å•å‘ä¼ é€’ï¼Œå组件会深拷è´çˆ¶ç»„ä»¶æ•°æ®ï¼Œå¯ä»Žçˆ¶ç»„件更新,也å¯è‡ªå·±æ›´æ–°æ•°æ®ï¼Œä½†ä¸ä¼šåŒæ¥çˆ¶ç»„ä»¶æ•°æ®ã€‚ * @Link是父åé—´åŒå‘ä¼ é€’ï¼Œçˆ¶ç»„ä»¶æ”¹å˜ï¼Œä¼šé€šçŸ¥æ‰€æœ‰çš„@Linkï¼ŒåŒæ—¶@Link的更新也会通知父组件对应å˜é‡è¿›è¡Œåˆ·æ–°ã€‚ ```typescript @Component struct TodoComponent { build() { Row() { Text('全部待办') .fontSize(30) .fontWeight(FontWeight.Bold) } .width('100%') .margin({top: 10, bottom: 10}) } } @Component struct AllChooseComponent { @Link isFinished: boolean; build() { Row() { Button('全选', {type: ButtonType.Normal}) .onClick(() => { this.isFinished = !this.isFinished; }) .fontSize(30) .fontWeight(FontWeight.Bold) .backgroundColor('#f7f6cc74') } .padding({left: 15}) .width('100%') .margin({top: 10, bottom: 10}) } } @Component struct ThingsComponent1 { @Prop isFinished: boolean; build() { // 待办事项1 Row({space: 15}) { if (this.isFinished) { // æ¤å¤„'app.media.finished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.finished')) .width(28) .height(28) } else { // æ¤å¤„'app.media.unfinished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.unfinished')) .width(28) .height(28) } Text('å¦ä¹ è¯æ–‡') .fontSize(24) .fontWeight(450) .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) } .height('40%') .width('100%') .border({width: 5}) .padding({left: 15}) .onClick(() => { this.isFinished = !this.isFinished; }) } } @Component struct ThingsComponent2 { @Prop isFinished: boolean; build() { // 待办事项1 Row({space: 15}) { if (this.isFinished) { // æ¤å¤„'app.media.finished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.finished')) .width(28) .height(28) } else { // æ¤å¤„'app.media.unfinished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.unfinished')) .width(28) .height(28) } Text('å¦ä¹ 高数') .fontSize(24) .fontWeight(450) .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) } .height('40%') .width('100%') .border({width: 5}) .padding({left: 15}) .onClick(() => { this.isFinished = !this.isFinished; }) } } @Entry @Component struct Index { @State isFinished: boolean = false; build() { Column() { // 全部待办 TodoComponent() // 全选 AllChooseComponent({isFinished: this.isFinished}) // 待办事项1 ThingsComponent1({isFinished: this.isFinished}) // 待办事项2 ThingsComponent2({isFinished: this.isFinished}) } .height('100%') .width('100%') .margin({top: 5, bottom: 5}) .backgroundColor('#90f1f3f5') } } ``` 效果图如下:  ### 循环渲染组件 * 上个示例虽然拆分出了å组件,但是å‘现组件1和组件2的代ç å分类似,当渲染的组件除了数æ®å¤–å…¶ä»–è®¾ç½®éƒ½ç›¸åŒæ—¶ï¼Œæ¤æ—¶å°±éœ€è¦ä½¿ç”¨åˆ°ForEach循环渲染。 * ForEach使用之åŽï¼Œå†—余代ç å˜å¾—更少,并且代ç ç»“æž„æ›´åŠ æ¸…æ™°ã€‚ ```typescript @Component struct TodoComponent { build() { Row() { Text('全部待办') .fontSize(30) .fontWeight(FontWeight.Bold) } .width('100%') .margin({top: 10, bottom: 10}) } } @Component struct AllChooseComponent { @Link isFinished: boolean; build() { Row() { Button('全选', {type: ButtonType.Normal}) .onClick(() => { this.isFinished = !this.isFinished; }) .fontSize(30) .fontWeight(FontWeight.Bold) .backgroundColor('#f7f6cc74') } .padding({left: 15}) .width('100%') .margin({top: 10, bottom: 10}) } } @Component struct ThingsComponent { @Prop isFinished: boolean; @Prop things: string; build() { // 待办事项1 Row({space: 15}) { if (this.isFinished) { // æ¤å¤„'app.media.finished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.finished')) .width(28) .height(28) } else { // æ¤å¤„'app.media.unfinished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ Image($r('app.media.unfinished')) .width(28) .height(28) } Text(`${this.things}`) .fontSize(24) .fontWeight(450) .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) } .height('8%') .width('90%') .padding({left: 15}) .opacity(this.isFinished ? 0.3: 1) .border({width:1}) .borderColor(Color.White) .borderRadius(25) .backgroundColor(Color.White) .onClick(() => { this.isFinished = !this.isFinished; }) } } @Entry @Component struct Index { @State isFinished: boolean = false; @State planList: string[] = [ '7.30 起床', '8.30 æ—©é¤', '11.30 ä¸é¤', '17.30 晚é¤', '21.30 夜宵', '22.30 洗澡', '1.30 起床' ]; build() { Column() { // 全部待办 TodoComponent() // 全选 AllChooseComponent({isFinished: this.isFinished}) List() { ForEach(this.planList, (item: string) => { // 待办事项1 ThingsComponent({isFinished: this.isFinished, things: item}) .margin(5) }) } } .height('100%') .width('100%') .margin({top: 5, bottom: 5}) .backgroundColor('#90f1f3f5') } } ``` 效果图如下:  ### @Builder方法 * Builder方法用于组件内定义方法,å¯ä»¥ä½¿å¾—相åŒä»£ç å¯ä»¥åœ¨ç»„件内进行å¤ç”¨ã€‚ * 本示例ä¸ä»…使用了@Builder方法进行去é‡ï¼ŒåŒæ—¶å¯¹æ•°æ®è¿›è¡Œäº†ç§»å‡ºï¼Œå¯ä»¥çœ‹åˆ°æ¤æ—¶ä»£ç æ›´åŠ æ¸…æ™°æ˜“è¯»ï¼Œç›¸å¯¹äºŽæœ€å¼€å§‹çš„ä»£ç ,@Entry组件基本åªç”¨äºŽå¤„ç†é¡µé¢æž„建逻辑,而ä¸å¤„ç†å¤§é‡ä¸Žé¡µé¢è®¾è®¡æ— 关的内容。 ```typescript @Observed class TodoListData { planList: string[] = [ '7.30 起床', '8.30 æ—©é¤', '11.30 ä¸é¤', '17.30 晚é¤', '21.30 夜宵', '22.30 洗澡', '1.30 起床' ]; } @Component struct TodoComponent { build() { Row() { Text('全部待办') .fontSize(30) .fontWeight(FontWeight.Bold) } .width('100%') .margin({top: 10, bottom: 10}) } } @Component struct AllChooseComponent { @Link isFinished: boolean; build() { Row() { Button('全选', {type: ButtonType.Capsule}) .onClick(() => { this.isFinished = !this.isFinished; }) .fontSize(30) .fontWeight(FontWeight.Bold) .backgroundColor('#f7f6cc74') } .padding({left: 15}) .width('100%') .margin({top: 10, bottom: 10}) } } @Component struct ThingsComponent { @Prop isFinished: boolean; @Prop things: string; @Builder displayIcon(icon: Resource) { Image(icon) .width(28) .height(28) .onClick(() => { this.isFinished = !this.isFinished; }) } build() { // 待办事项1 Row({space: 15}) { if (this.isFinished) { // æ¤å¤„'app.media.finished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ this.displayIcon($r('app.media.finished')); } else { // æ¤å¤„'app.media.unfinished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ this.displayIcon($r('app.media.unfinished')); } Text(`${this.things}`) .fontSize(24) .fontWeight(450) .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) .onClick(() => { this.things += '啦'; }) } .height('8%') .width('90%') .padding({left: 15}) .opacity(this.isFinished ? 0.3: 1) .border({width:1}) .borderColor(Color.White) .borderRadius(25) .backgroundColor(Color.White) } } @Entry @Component struct Index { @State isFinished: boolean = false; @State data: TodoListData = new TodoListData(); build() { Column() { // 全部待办 TodoComponent() // 全选 AllChooseComponent({isFinished: this.isFinished}) List() { ForEach(this.data.planList, (item: string) => { // 待办事项1 ThingsComponent({isFinished: this.isFinished, things: item}) .margin(5) }) } } .height('100%') .width('100%') .margin({top: 5, bottom: 5}) .backgroundColor('#90f1f3f5') } } ``` 效果图如下:  ### 总结 * 通过对代ç ç»“æž„çš„ä¸€æ¥æ¥ä¼˜åŒ–,å¯ä»¥çœ‹åˆ°@Enrty组件作为页é¢çš„å…¥å£ï¼Œå…¶build函数应该åªéœ€è¦è€ƒè™‘将需è¦çš„组件进行组åˆï¼Œç±»ä¼¼äºŽæç§¯æœ¨ï¼Œå°†éœ€è¦çš„组件æèµ·æ¥ã€‚被page调用的å组件则类似积木,ç‰ç€è¢«éœ€è¦çš„page进行调用。状æ€å˜é‡ç±»ä¼¼äºŽç²˜åˆå‰‚,当触å‘UI刷新事件时,状æ€å˜é‡èƒ½è‡ªåŠ¨å®Œæˆå¯¹åº”绑定的组件的刷新,从而实现page的按需刷新。 * 虽然现有的架构并未使用到MVVM的设计ç†å¿µï¼Œä½†æ˜¯MVVMçš„æ ¸å¿ƒç†å¿µå·²ç»å‘¼ä¹‹æ¬²å‡ºï¼Œè¿™ä¹Ÿæ˜¯ä¸ºä»€ä¹ˆè¯´ArkUIçš„UIå¼€å‘天生属于MVVM模å¼ï¼Œpage和组件就是View层,pageè´Ÿè´£æç§¯æœ¨ï¼Œç»„件就是积木被page组织;组件需è¦åˆ·æ–°ï¼Œé€šè¿‡çжæ€å˜é‡é©±åŠ¨ç»„ä»¶åˆ·æ–°ä»Žè€Œæ›´æ–°pageï¼›ViewModel的数æ®éœ€è¦æœ‰æ¥æºï¼Œè¿™å°±æ˜¯Modelå±‚æ¥æºã€‚ * 示例ä¸çš„代ç 功能还是比较简å•çš„ï¼Œä½†æ˜¯å·²ç»æ„Ÿè§‰åˆ°åŠŸèƒ½è¶Šæ¥è¶Šå¤šçš„æƒ…况下,主page的代ç è¶Šæ¥è¶Šå¤šï¼Œå½“å¤‡å¿˜å½•éœ€è¦æ·»åŠ çš„åŠŸèƒ½è¶Šæ¥è¶Šå¤šæ—¶ï¼Œå…¶ä»–çš„page也需è¦ä½¿ç”¨åˆ°ä¸»page的组件时,应该如何去组织项目结构呢,MVVMæ¨¡å¼æ˜¯ç»„织的首选。 ## 通过MVVMå¼€å‘备忘录实战 ä¸Šä¸€ç« èŠ‚ä¸ï¼Œå±•示了éžMVVM模å¼å¦‚何组织代ç ,能感觉到éšç€ä¸»page的代ç è¶Šæ¥è¶Šåºžå¤§ï¼Œåº”该采å–åˆç†çš„æ–¹å¼è¿›è¡Œåˆ†å±‚,使得项目结构清晰,组件之间ä¸åŽ»äº’ç›¸å¼•ç”¨ï¼Œå¯¼è‡´åŽæœŸç»´æŠ¤æ—¶ç‰µä¸€å‘è€ŒåŠ¨å…¨èº«ï¼ŒåŠ å¤§åŽæœŸåŠŸèƒ½æ›´æ–°çš„å›°éš¾ï¼Œä¸ºæ¤æœ¬ç« 通过对MVVMçš„æ ¸å¿ƒæ–‡ä»¶ç»„ç»‡æ¨¡å¼ä»‹ç»å…¥æ‰‹ï¼Œå‘å¼€å‘者展示如何使用MVVMæ¥ç»„ç»‡ä¸Šä¸€ç« èŠ‚çš„ä»£ç 。 ### MVVM文件结构说明 * src * ets * pages ------ å˜æ”¾é¡µé¢ç»„ä»¶ * views ------ å˜æ”¾ä¸šåŠ¡ç»„ä»¶ * shares ------ å˜æ”¾é€šç”¨ç»„ä»¶ * service ------ æ•°æ®æœåŠ¡ * app.ts ------ æœåŠ¡å…¥å£ * LoginViewMode ----- 登录页ViewModel * xxxModel ------ 其他页ViewModel ### 分层设计技巧 **Model层** * modelå±‚å˜æ”¾æœ¬åº”ç”¨æ ¸å¿ƒæ•°æ®ç»“构,这层本身和UIå¼€å‘关系ä¸å¤§ï¼Œè®©ç”¨æˆ·æŒ‰ç…§è‡ªå·±çš„业务逻辑进行å°è£…。 **ViewModel层** > 注æ„: > > ViewModel层ä¸åªæ˜¯å˜æ”¾æ•°æ®ï¼Œä»–åŒæ—¶éœ€è¦æä¾›æ•°æ®çš„æœåŠ¡åŠå¤„ç†ï¼Œå› æ¤å¾ˆå¤šæ¡†æž¶ä¼šä»¥â€œserviceâ€æ¥è¿›è¡Œè¡¨è¾¾æ¤å±‚。 * ViewModel层是为视图æœåŠ¡çš„æ•°æ®å±‚。它的设计一般æ¥è¯´ï¼Œæœ‰ä¸¤ä¸ªç‰¹ç‚¹ï¼š 1ã€æŒ‰ç…§é¡µé¢ç»„织数æ®ã€‚ 2ã€æ¯ä¸ªé¡µé¢æ•°æ®è¿›è¡Œæ‡’åŠ è½½ã€‚ **View层** Viewå±‚æ ¹æ®éœ€è¦æ¥ç»„织,但View层需è¦åŒºåˆ†ä¸€ä¸‹ä¸‰ç§ç»„件: * 页é¢ç»„件:æä¾›æ•´ä½“页é¢å¸ƒå±€ï¼Œå®žçŽ°å¤šé¡µé¢ä¹‹é—´çš„跳转,å‰åŽå°äº‹ä»¶å¤„ç†ç‰é¡µé¢å†…容。 * 业务组件:被页é¢å¼•用,构建出页é¢ã€‚ * å…±äº«ç»„ä»¶ï¼šä¸Žé¡¹ç›®æ— å…³çš„å¤šé¡¹ç›®å…±äº«ç»„ä»¶ã€‚ > 共享组件和业务组件的区别: > > 业务组件包å«äº†ViewModel层数æ®ï¼Œæ²¡æœ‰ViewModel,这个组件ä¸èƒ½è¿è¡Œã€‚ > > 共享组件:ä¸åŒ…å«ä»»åŠ¡ViewModel层的数æ®ï¼Œä»–需è¦çš„æ•°æ®éœ€è¦ä»Žå¤–éƒ¨ä¼ å…¥ã€‚å…±äº«ç»„ä»¶åŒ…å«ä¸€ä¸ªè‡ªåŒ…å«ç»„件,åªè¦å¤–éƒ¨å‚æ•°ï¼ˆæ— ä¸šåŠ¡å‚æ•°ï¼‰æ»¡è¶³ï¼Œå°±å¯ä»¥å·¥ä½œã€‚ ### 代ç 示例 现在按照MVVM模å¼ç»„ç»‡ç»“æž„ï¼Œé‡æž„如下: * src * ets * pages * index * View * TodoComponent * AllchooseComponent * ThingsComponent * ViewModel * ThingsViewModel 文件代ç 如下: * Index.ets ```typescript // import view import { TodoComponent } from './../View/TodoComponent' import { MultiChooseComponent } from './../View/AllchooseComponent' import { ThingsComponent } from './../View/ThingsComponent' // import viewModel import { TodoListData } from '../ViewModel/ThingsViewModel' @Entry @Component struct Index { @State isFinished: boolean = false; @State data: TodoListData = new TodoListData(); build() { Column() { Row({space: 40}) { // 全部待办 TodoComponent() // 全选 MultiChooseComponent({isFinished: this.isFinished}) } List() { ForEach(this.data.planList, (item: string) => { // 待办事项1 ThingsComponent({isFinished: this.isFinished, things: item}) .margin(5) }) } } .height('100%') .width('100%') .margin({top: 5, bottom: 5}) .backgroundColor('#90f1f3f5') } } ``` * TodoComponent ```typescript @Component export struct TodoComponent { build() { Row() { Text('全部待办') .fontSize(30) .fontWeight(FontWeight.Bold) } .padding({left: 15}) .width('50%') .margin({top: 10, bottom: 10}) } } ``` * AllchooseComponent.ets ```typescript @Component export struct MultiChooseComponent { @Link isFinished: boolean; build() { Row() { Button('多选', {type: ButtonType.Capsule}) .onClick(() => { this.isFinished = !this.isFinished; }) .fontSize(30) .fontWeight(FontWeight.Bold) .backgroundColor('#f7f6cc74') } .padding({left: 15}) .width('100%') .margin({top: 10, bottom: 10}) } } ``` * ThingsComponent ```typescript @Component export struct ThingsComponent { @Prop isFinished: boolean; @Prop things: string; @Builder displayIcon(icon: Resource) { Image(icon) .width(28) .height(28) .onClick(() => { this.isFinished = !this.isFinished; }) } build() { // 待办事项1 Row({space: 15}) { if (this.isFinished) { // æ¤å¤„'app.media.finished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ this.displayIcon($r('app.media.finished')); } else { // æ¤å¤„'app.media.unfinished'仅作示例,请开å‘者自行替æ¢ï¼Œå¦åˆ™imageSource创建失败会导致åŽç»æ— 法æ£å¸¸æ‰§è¡Œã€‚ this.displayIcon($r('app.media.unfinished')); } Text(`${this.things}`) .fontSize(24) .fontWeight(450) .decoration({type: this.isFinished ? TextDecorationType.LineThrough : TextDecorationType.None}) .onClick(() => { this.things += '啦'; }) } .height('8%') .width('90%') .padding({left: 15}) .opacity(this.isFinished ? 0.3: 1) .border({width:1}) .borderColor(Color.White) .borderRadius(25) .backgroundColor(Color.White) } } ``` ThingsViewModel.ets ```typescript @Observed export class TodoListData { planList: string[] = [ '7.30 起床', '8.30 æ—©é¤', '11.30 ä¸é¤', '17.30 晚é¤', '21.30 夜宵', '22.30 洗澡', '1.30 起床' ]; } ``` ç»è¿‡MVVMæ¨¡å¼æ‹†åˆ†ä¹‹åŽçš„代ç ï¼Œé¡¹ç›®ç»“æž„æ›´åŠ æ¸…æ™°ï¼Œå„个模å—çš„èŒè´£æ›´åŠ æ¸…æ™°ï¼Œå‡å¦‚有新的page需è¦ç”¨åˆ°äº‹ä»¶è¿™ä¸ªç»„件,åªéœ€è¦import对应的组件å³å¯ï¼Œå› 为是固定的本地数æ®ï¼Œæ²¡æœ‰å޻写Model层的逻辑,åŽç»å¼€å‘者也å¯ä»¥ç…§ç€ç¤ºä¾‹å޻釿ž„自己的项目结构。 效果图如下: 