基于 HarmonyOS 5.0 的美食抽签应用开发实践
前言
本文记录了一个基于 HarmonyOS 5.0 开发的美食抽签应用实践经验。这是一个解决"吃什么"日常困扰的应用,具有以下特点:
-
核心功能
- 支持早中晚餐分类管理
- 提供随机抽签选择功能
- 记录历史抽签数据
- 展示数据统计分析
-
交互设计
- 流畅的抽签动画效果
- 精美的食物图标展示
- 符合直觉的手势操作
- 清晰的数据可视化
-
技术特点
- 采用 Stage 模型的单 Ability 架构
- 使用 ArkTS 声明式 UI 开发
- 基于首选项的数据持久化
- 支持深浅色主题切换
-
项目规范
- 遵循 TypeScript 开发规范
- 采用组件化开发方案
- 统一的错误处理机制
- 规范的代码组织结构
功能模块
1. 抽签模块
- 支持按餐点类型抽签
- 提供抽签动画效果
- 记录抽签历史
- 展示抽签结果
2. 管理模块
- 食物分类管理
- 自定义添加食物
- 食物图标选择
- 批量导入导出
3. 统计模块
- 展示抽签频率统计
- 提供历史记录查询
- 数据可视化图表
- 使用习惯分析
开发环境
- HarmonyOS SDK: 5.0.0
- DevEco Studio: 5.0
- Node.js: 18.12.0
- ohpm: 1.0.0
技术栈
- 开发框架:ArkTS + HarmonyOS 5.0
- UI 框架:ArkUI 5.0
- 状态管理:@State、@Link、@Prop、@Provide/@Consume
- 数据持久化:Data Preferences
- 动画系统:ArkTS Animation API
项目结构
entry/src/main/ets/
├── pages/ # 页面目录
│ ├── Index.ets # 主页面
│ └── tabs/ # 标签页
│ ├── DrawPage.ets # 抽签页面
│ ├── ManagePage.ets# 管理页面
│ └── StatsPage.ets # 统计页面
├── services/ # 服务层
│ ├── FoodListService.ets # 食物列表服务
│ └── FoodDataService.ets # 数据服务
├── components/ # 组件目录
│ ├── FoodCard.ets # 食物卡片组件
│ └── StatChart.ets # 统计图表组件
├── common/ # 公共目录
│ ├── constants/ # 常量定义
│ └── utils/ # 工具函数
└── models/ # 数据模型
└── FoodModel.ets # 食物数据模型
数据结构设计
// 食物数据模型
interface FoodItem {
id: string // 唯一标识
name: string // 食物名称
type: MealType // 餐点类型
icon?: string // 食物图标
createTime: number // 创建时间
}
// 餐点类型枚举
enum MealType {
BREAKFAST = 'breakfast',
LUNCH = 'lunch',
DINNER = 'dinner',
}
// 抽签记录
interface DrawRecord {
id: string // 记录ID
foodId: string // 食物ID
time: number // 抽签时间
type: MealType // 餐点类型
}
// 统计数据
interface FoodStat {
foodId: string // 食物ID
count: number // 抽签次数
lastTime: number // 最后抽签时间
}
核心功能实现
1. Stage 模型应用
import AbilityConstant from '@ohos.app.ability.AbilityConstant'
import UIAbility from '@ohos.app.ability.UIAbility'
import window from '@ohos.window'
export default class EntryAbility extends UIAbility {
onCreate(want: Want) {
// 应用初始化
}
onWindowStageCreate(windowStage: window.WindowStage) {
// 加载主页面
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error('Failed to load content', err.code)
return
}
})
}
}
2. 数据服务实现
export class FoodDataService {
private static instance: FoodDataService
private preferences: data_preferences.Preferences | null = null
private readonly STORE_NAME: string = 'FoodPreferences'
public static getInstance(): FoodDataService {
if (!FoodDataService.instance) {
FoodDataService.instance = new FoodDataService()
}
return FoodDataService.instance
}
public async getFoodsByType(type: MealType): Promise<FoodItem[]> {
const foods = await this.getAllFoods()
return foods.filter((item) => item.type === type)
}
private async ensureInitialized(): Promise<void> {
if (!this.preferences) {
await this.init()
}
}
}
3. 自定义组件封装
@Component
struct FoodCard {
@Prop food: FoodItem
@State private isPressed: boolean = false
@Consume('themeData') theme: Theme
build() {
Row() {
Text(this.food.icon)
.fontSize(24)
Text(this.food.name)
.fontSize(16)
.fontColor(this.theme.textColor)
}
.width('100%')
.padding(16)
.backgroundColor(this.isPressed ?
this.theme.pressedColor :
this.theme.backgroundColor)
.borderRadius(8)
.stateStyles({
pressed: {
transform: { scale: 0.98 },
opacity: 0.8
}
})
}
}
4. 性能优化实践
- 列表性能优化:
@Component
struct OptimizedList {
@State private foodList: FoodItem[] = []
build() {
List({ space: 12 }) {
LazyForEach(this.foodList,
(food: FoodItem) => this.ListItem(food),
(food: FoodItem) => food.id
)
}
.onScrollIndex((first: number, last: number) => {
this.handleVisibleItemsChange(first, last)
})
}
@Builder
private ListItem(food: FoodItem) {
Row() {
Text(food.icon)
.fontSize(24)
Text(food.name)
.fontSize(16)
}
.width('100%')
.padding(16)
}
}
- 启动优化:
aboutToAppear() {
// 延迟加载非关键资源
setTimeout(() => {
this.loadNonCriticalResources()
}, 0)
// 并行加载数据
Promise.all([
this.loadFoodList(),
this.loadUserPreferences(),
this.loadThemeSettings()
]).catch(err => {
console.error('Failed to load initial data:', err)
})
}
5. 错误处理
class ErrorHandler {
static handle(error: Error, context: string) {
Logger.error(`[${context}]`, error)
if (error instanceof NetworkError) {
this.showToast('网络连接失败,请检查网络设置')
} else if (error instanceof DataError) {
this.showToast('数据加载失败,请稍后重试')
}
}
private static showToast(message: string) {
promptAction.showToast({
message: message,
duration: 3000,
bottom: '100vp',
})
}
}
6. 主题系统实现
// 主题配置
interface ThemeConfig {
colorMode: 'light' | 'dark'
primaryColor: string
backgroundColor: string
textColor: string
cardColor: string
}
@Component
struct ThemeProvider {
@State themeConfig: ThemeConfig = defaultTheme
@Builder
renderContent() {
Column() {
// 页面内容
}
.backgroundColor(this.themeConfig.backgroundColor)
.width('100%')
.height('100%')
}
build() {
Column() {
this.renderContent()
}
}
}
7. 动画效果实现
@Component
struct DrawAnimation {
@State private rotateAngle: number = 0
@State private scale: number = 1
// 抽签动画
private startAnimation() {
animateTo({
duration: 1000,
curve: Curve.EaseInOut,
onFinish: () => {
this.handleAnimationComplete()
}
}, () => {
this.rotateAngle = 360
this.scale = 1.2
})
}
build() {
Stack() {
// 动画内容
}
.rotate({ x: 0, y: 1, z: 0, angle: this.rotateAngle })
.scale({ x: this.scale, y: this.scale })
}
}
8. 统计图表实现
@Component
struct StatisticsChart {
@State private data: FoodStat[] = []
private readonly chartWidth: number = 300
private readonly chartHeight: number = 200
private calculateBarHeight(count: number): number {
const maxCount = Math.max(...this.data.map(item => item.count))
return maxCount === 0 ? 0 : (count / maxCount) * this.chartHeight
}
build() {
Column() {
Row() {
ForEach(this.data, (item: FoodStat) => {
Column() {
Text(item.count.toString())
.fontSize(12)
Column()
.width(20)
.height(this.calculateBarHeight(item.count))
.backgroundColor('#FF4B4B')
Text(item.name)
.fontSize(12)
}
})
}
.width(this.chartWidth)
.height(this.chartHeight)
}
}
}
实现要点
-
数据管理
- 使用单例模式管理全局数据
- 采用 Data Preferences 实现数据持久化
- 实现类型安全的数据操作
-
UI 实现
- 基于 ArkTS 声明式 UI 开发
- 自定义组件封装复用
- 响应式布局适配
-
性能优化
- 使用 LazyForEach 实现虚拟列表
- 延迟加载非关键资源
- 优化动画性能
-
工程规范
- 统一的错误处理机制
- 规范的代码组织结构
- 完善的类型定义
开发心得
- Stage 模型简化了应用架构设计
- ArkTS 提供了优秀的开发体验
- 组件化开发提高了代码复用率
- 性能优化需要注意细节
参考资源
关于作者
专注 HarmonyOS 应用开发,欢迎交流讨论:
- gitee:https://gitee.com/zhaoyl1/what-to-eat-meta-service/settings#index