| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 | ||
| 9 个月前 |
电影选座示例
made by 林子淇
本项目是 HarmonyOS 官方 电影选座 ArkTS 示例 的仓颉语言重写版本。旨在演示如何使用仓颉鸿蒙构建一个功能完整的电影选座应用场景。
电影选座是便捷生活类应用中的典型交互场景。用户可以直观地看到影厅的座位布局,根据座位的不同类型(如黄金区、特惠区)进行选择。用户可以点击座位进行选择、再次点击取消选择,并在底部信息卡片中看到已选座位的列表,最后进行确认。
在仓颉鸿蒙开发方面,本项目旨在清晰地演示声明式UI的两个关键概念:
- 状态驱动UI更新:通过
@Observed和@Publish宏,实现数据状态与UI视图的自动同步。 - 组件间状态共享:利用
LocalStorage机制,让分散在不同组件中的UI元素能够安全、高效地共享和响应同一份数据源。
在此基础上,您可以参考股市图表示例,它将声明式UI与命令式UI作对比,增强了对 Canvas 组件的命令式绘图控制和数据交互的演示,实现了更复杂的 Canvas 组件的使用场景。
效果预览

实现思路
项目整体遵循了经典的 MVVM (Model-View-ViewModel) 架构模式,确保了UI、状态和业务逻辑的清晰分离。
1. 状态管理与组件间共享
在声明式UI编程中,我们不直接操作UI组件,而是通过改变状态来驱动UI的自动更新,即 view = f(state),其中 f 由框架实现,并向开发者隐藏。
-
使用
@Observed和@Publish声明状态UI的“长相”完全由数据状态决定。当数据变化时,UI会自动刷新以匹配新的数据。
- 状态定义: 在
SeatSelection.cj(ViewModel) 中,我们将整个类标记为@Observed,使其成为一个可观察的数据源。其中,直接影响UI呈现的seatsState(座位图状态)和selectedSeats(已选座位列表)两个变量,我们使用@Publish宏进行标记。这意味着任何对这两个变量的修改都将被UI框架侦听。 - 状态消费: 在UI组件中,我们直接使用这些状态来构建界面。例如,
SeatSelectionBar.cj组件根据viewModel.selectedSeats数组的大小来决定显示“推荐座位”还是已选座位列表。同样,Card.cj中的“确认”按钮也通过.enabled(viewModel.selectedSeats.size > 0)来动态控制其是否可点击。 - 自动更新: 当用户点击座位,ViewModel 更新了
selectedSeats数组(比如,向其中添加一个座位)后,仓颉的UI框架会自动检测到变化,并重新渲染所有依赖selectedSeats的UI部分。开发者无需编写任何手动刷新UI的代码。
// entry/src/main/cangjie/view_model/SeatSelection.cj @Observed public class SeatSelection { // @Publish 标记此状态的变更需要通知UI @Publish public var seatsState: ObservedArrayList<ObservedArray<SEAT_TYPE>> = ... @Publish public var selectedSeats: ObservedArrayList<SeatInfo> = ObservedArrayList<SeatInfo>() // ... } // entry/src/main/cangjie/components/SeatSelectionBar.cj @Component public class SeatSelectionBar { @LocalStorageLink['viewModel'] // 链接到共享的状态 var viewModel: SeatSelection = SeatSelection() func build() { // UI直接消费状态,根据数组大小决定显示内容 if (viewModel.selectedSeats.size == 0) { Text("推荐座位") // ... } else { ForEach(viewModel.selectedSeats, { seat: SeatInfo, _: Int64 => SelectedSeat(seatInfo: seat) }) } } } - 状态定义: 在
-
使用
LocalStorage实现状态共享为了让
SeatCanvas、SeatSelectionBar、Card等多个独立的组件能访问和修改同一个SeatSelection实例,我们使用了LocalStorage。- 创建存储: 在应用的入口
index.cj中,我们创建了一个LocalStorage实例,并将其与根组件EntryView关联。 - 链接状态: 在需要访问共享状态的子组件中,我们使用
@LocalStorageLink['viewModel']宏来获取LocalStorage中名为 'viewModel' 的数据对象。这确保了所有组件操作的都是同一个 ViewModel 实例,从而实现了状态的全局共享。
// entry/src/main/cangjie/index.cj let storage = LocalStorage() // 1. 创建存储实例 @Entry[storage] // 2. 与根组件关联 @Component class EntryView { // ... } // entry/src/main/cangjie/components/Card.cj @Component public class Card { // 3. 在子组件中通过名称链接到该状态 @LocalStorageLink['viewModel'] var viewModel: SeatSelection = SeatSelection() // ... } - 创建存储: 在应用的入口
2. 核心交互的数据流
理解用户点击一个座位后发生的事情,是理解本项目数据流的关键。这个过程巧妙地结合了命令式UI(手动绘制Canvas)和声明式UI(自动更新组件)的优点。
- 用户操作: 用户在
SeatCanvas组件上点击。 - 事件处理:
Canvas的onClick事件被触发,调用viewModel.handleCanvasClick(evt)。 - ViewModel 状态更新:
handleCanvasClick方法计算出用户点击的是哪个座位(行和列)。- 它接着调用
handleSeatClick(seatInfo),这是核心的业务逻辑。 handleSeatClick方法会修改两个核心状态:- 更新
seatsState数组,将被点击座位的状态改为SELECTED或改回原始状态。 - 更新
selectedSeats列表,添加或移除对应的SeatInfo。
- 更新
- UI 响应:
- 命令式更新 (Canvas): ViewModel 在更新完状态后,会调用一个预先注册的回调函数
onStateChangeCallback。SeatCanvas组件在初始化时将自己的drawSeat方法注册为这个回调。因此,ViewModel 的调用会让SeatCanvas对指定的座位进行重绘。 - 声明式更新 (其他组件): 与此同时,由于
@Publish标记的selectedSeats数组发生了变化,UI框架会自动地重新渲染所有依赖它的组件。这包括:SeatSelectionBar会从“推荐座位”视图切换到已选座位列表。Card组件中的“确认”按钮会根据selectedSeats是否为空,自动变为可用或禁用状态。
- 命令式更新 (Canvas): ViewModel 在更新完状态后,会调用一个预先注册的回调函数
通过这种方式,应用实现了高效和易于维护的完美结合:对于复杂的、需要精细控制的 Canvas 采用手动重绘,而对于常规的UI组件则充分享受声明式编程带来的便利。
工程目录
├── entry/src/main/cangjie
│ ├── MainAbility.cj
│ ├── AbilityStage.cj
│ ├── index.cj
│ ├── constants // 常量
│ │ ├── AreaInfo.cj
│ │ ├── DataConstants.cj
│ │ ├── SeatInfo.cj
│ │ ├── SeatType.cj
│ │ └── StyleConstants.cj
│ ├── view_model // 视图模型
│ │ └── SeatSelection.cj
│ └── components // UI组件
│ ├── Card.cj
│ ├── MovieInfo.cj
│ ├── MyDialog.cj
│ ├── SeatCanvas.cj // 核心的座位图Canvas组件
│ ├── SeatMap.cj
│ ├── SeatSelectionBar.cj // 底部已选座位/推荐座位滚动条
│ ├── SelectedSeat.cj
│ ├── tag.cj
│ └── title.cj
└── entry/src/main/resources // 应用资源目录