鸿蒙原生应用从0到1:项目搭建与首页开发实战
鸿蒙原生应用从0到1:项目搭建与首页开发实战
本系列文章将带大家从零开发一个完整的鸿蒙原生应用——「生活助手」,涵盖项目搭建、页面开发、状态管理、数据交互等核心实战内容。
一、写在前面
鸿蒙生态发展至今,原生应用开发已经不再是"要不要学"的问题,而是"什么时候开始学"的问题。作为一名移动端开发者,我决定用 ArkTS + Stage 模型开发一个完整的「生活助手」App,把我的学习和实战过程记录下来,希望对同样在学习的你有所帮助。
本文是系列第一篇,重点讲解项目搭建、工程结构设计、以及首页的开发实现。
二、项目概况
「生活助手」是一个集待办管理、记账理财、备忘录、心情日记、个人中心于一体的日常管理工具,涵盖了一个完整 App 开发中的核心场景:
| 功能模块 | 核心能力 | 技术亮点 |
|---|---|---|
| 首页仪表盘 | 每日一言、统计概览、快捷入口、动态流 | Scroll + 组合 Builder |
| 待办管理 | 分类筛选、优先级标记、添加/删除/切换完成 | 状态管理 + 过滤逻辑 |
| 记账本 | 收支汇总、分类统计、记录管理 | 数据聚合计算 |
| 备忘录 | 分类筛选、搜索、详情查看、编辑 | 搜索逻辑 + 多视图切换 |
| 心情日记 | 日历视图、情绪记录、月度统计 | 日历算法 + 数据可视化 |
三、项目搭建 —— 从 DevEco Studio 开始
3.1 创建项目
打开 DevEco Studio,选择File → New → Create Project:
- 模板选择:Empty Ability(Stage 模型)
- 兼容版本:API 9+(我选择 API 23,对应新版 SDK)
- 包名:
com.example.myapplication - 项目位置:自行选择
创建完成后,你会看到如下工程结构:
MyApplication/ ├── AppScope/ # 全局应用配置 │ ├── app.json5 # 应用级配置 │ └── resources/ # 全局资源 ├── entry/ # 应用主模块 │ ├── src/ │ │ ├── main/ │ │ │ ├── ets/ # 代码目录 │ │ │ │ ├── entryability/ # Ability 生命周期 │ │ │ │ └── pages/ # 页面文件 │ │ │ ├── resources/ # 资源文件 │ │ │ └── module.json5 # 模块配置 │ ├── build-profile.json5 # 构建配置 │ └── oh-package.json5 # 包依赖 ├── hvigor/ # 构建工具配置 ├── build-profile.json5 # 全局构建配置 └── oh-package.json5 # 全局包依赖3.2 重要配置文件解读
AppScope/app.json5 —— 应用级配置
{"app":{"bundleName":"com.example.myapplication","vendor":"example","versionCode":1000000,"versionName":"1.0.0","icon":"$media:layered_image","label":"$string:app_name"}}这里配置了应用的包名、版本号、图标和显示名称。bundleName是应用的唯一标识,一旦发布不可修改。
entry/src/main/module.json5 —— 模块配置
{"module":{"name":"entry","type":"entry","deviceTypes":["phone"],"pages":"$profile:main_pages","abilities":[{"name":"EntryAbility","srcEntry":"./ets/entryability/EntryAbility.ets","exported":true,"skills":[{"entities":["entity.system.home"],"actions":["ohos.want.action.home"]}]}]}}关键点:
deviceTypes:声明支持的设备类型,这里仅支持 phonepages:引用$profile:main_pages,对应resources/base/profile/main_pages.jsonskills:声明该 Ability 可以响应桌面启动意图
main_pages.json —— 页面路由注册
{"src":["pages/Index","pages/TodoPage","pages/FinancePage","pages/NotePage","pages/MoodPage","pages/ProfilePage"]}所有页面都必须在这里注册,否则无法通过router.pushUrl()跳转。
四、资源体系 —— 颜色、字号、字符串
鸿蒙应用推荐使用资源引用($r)的方式来管理 UI 属性,而不是写死常量值。这样做的好处是:
- 支持深色模式自动切换(同一资源名,不同值)
- 支持多语言适配
- 修改一处,全局生效
4.1 颜色资源
// resources/base/element/color.json{"color":[{"name":"primary","value":"#5B7FFF"},{"name":"primary_light","value":"#E8ECFF"},{"name":"text_primary","value":"#1A1A2E"},{"name":"text_secondary","value":"#6B7280"},{"name":"bg_primary","value":"#F5F7FA"},{"name":"bg_card","value":"#FFFFFF"},{"name":"divider","value":"#E5E7EB"},{"name":"shadow","value":"#1A000000"}]}我还为深色模式单独配置了dark/element/color.json,应用会自动根据系统主题切换。
4.2 字号资源
// resources/base/element/float.json{"float":[{"name":"title_font_size","value":"24fp"},{"name":"subtitle_font_size","value":"18fp"},{"name":"body_font_size","value":"16fp"},{"name":"small_font_size","value":"14fp"},{"name":"tiny_font_size","value":"12fp"},{"name":"card_radius","value":"16vp"},{"name":"button_radius","value":"12vp"}]}注意:fp是鸿蒙特有的字体单位(类似 Android 的 sp),会跟随系统字体缩放;vp是虚拟像素单位(类似 dp)。
在代码中这样引用:
Text('标题').fontSize($r('app.float.title_font_size')).fontColor($r('app.color.text_primary'))五、EntryAbility —— 应用入口分析
// entryability/EntryAbility.etsexportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);}onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent('pages/Index',(err)=>{if(err.code){hilog.error(DOMAIN,'testTag','Failed to load content: %{public}s',JSON.stringify(err));}});}}核心逻辑:
onCreate:设置颜色模式为跟随系统(NOT_SET)onWindowStageCreate:加载首页pages/Index- 使用
hilog日志系统记录关键生命周期事件
六、首页开发 —— 生活助手仪表盘
首页是整个 App 的门面,我设计了一个信息型仪表盘,包含以下区域:
┌─────────────────────────────┐ │ 生活助手 2025年1月15日 │ ← 顶部标题栏 │ 👤│ ← 点击跳转个人中心 ├─────────────────────────────┤ │ 📖 每日一言 [换一句]│ ← 可切换的名人名言 │ "生活不止眼前的苟且..." │ │ —— 高晓松 │ ├─────────────────────────────┤ │ 📋 今日待办 │ 💰 本月支出 │ ← 2×2 统计卡片 │ 5项 │ 3,280元 │ │ 📝 备忘录 │ 🔥 连续打卡 │ │ 12篇 │ 7天 │ ├─────────────────────────────┤ │ ➕新增待办 💳记一笔 │ ← 快捷操作栏 │ ✏️写笔记 😊记心情 │ ├─────────────────────────────┤ │ 最近动态 │ ← 活动流 │ 📋 完成项目方案 10:30 │ │ 💰 午餐支出 12:00 │ │ 📝 读书笔记 14:30 │ └─────────────────────────────┘6.1 数据模型定义
ArkTS 中定义接口统一使用interface:
interfaceDailyQuote{text:string;author:string;}interfaceStatItem{title:string;value:string;unit:string;color:string;icon:string;}interfaceQuickAction{name:string;icon:string;page:string;color:string;}interfaceActivityItem{title:string;desc:string;time:string;type:string;// 'todo' | 'finance' | 'note' | 'mood'}6.2 状态与数据初始化
@Componentstruct Index{@StatecurrentQuoteIndex:number=0;@StatecurrentDate:string='';@StateuserName:string='用户';privatequotes:DailyQuote[]=[{text:'生活不止眼前的苟且,还有诗和远方',author:'高晓松'},{text:'千里之行,始于足下',author:'老子'},// ...];aboutToAppear():void{this.updateDate();}updateDate():void{constnow=newDate();constweekDays:string[]=['日','一','二','三','四','五','六'];this.currentDate=`${now.getFullYear()}年${now.getMonth()+1}月${now.getDate()}日 星期${weekDays[now.getDay()]}`;}}这里有一个细节:updateDate()在aboutToAppear()中调用,确保进入页面时日期是最新的。aboutToAppear是 ArkTS 的生命周期钩子,类似 Vue 的onMounted。
6.3 每日一言 —— 交互卡片
Column(){Row(){Text('📖 每日一言')Blank()Text('换一句').fontColor($r('app.color.primary')).onClick(()=>{this.currentQuoteIndex=(this.currentQuoteIndex+1)%this.quotes.length;})}Text(this.quotes[this.currentQuoteIndex].text).lineHeight(24)Text(`——${this.quotes[this.currentQuoteIndex].author}`)}通过currentQuoteIndex状态 + 取模运算实现循环切换。点击"换一句"会触发 UI 重新渲染,因为@State装饰器标记了该变量。
6.4 统计卡片 —— @Builder 复用
为了避免重复写相同的卡片布局,我使用了@Builder装饰器:
@BuilderstatCard(item:StatItem):void{Column(){Row(){Text(item.icon)Text(item.value).fontColor(item.color)}Row(){Text(item.title)Text(item.unit)}}}在 build 方法中直接调用:
Row(){ForEach(this.stats.slice(0,2),(item:StatItem)=>{this.statCard(item)},(item:StatItem)=>item.title)}ForEach的第三个参数是键生成函数,用于优化列表渲染性能,类似 React 的key。
6.5 路由跳转
// 导航到个人中心.onClick(()=>{router.pushUrl({url:'pages/ProfilePage'});})// 快捷操作.onClick(()=>{router.pushUrl({url:action.page});})注意:路由的url不需要.ets后缀,且路径相对于pages目录。需要在main_pages.json中注册。
6.6 活动流 —— 动态类型映射
getActivityIcon(type:string):string{consticons:Record<string,string>={'todo':'📋','finance':'💰','note':'📝','mood':'😊'};returnicons[type]||'📌';}getActivityColor(type:string):ResourceColor{constcolors:Record<string,string>={'todo':'#E8ECFF','finance':'#FFF3E0','note':'#E8F5E9','mood':'#F3E5F5'};returncolors[type]||'#F5F5F5';}这里使用了Record<string, string>类型来定义映射表,避免使用switch-case长篇代码,非常简洁。
七、技术总结
本篇核心知识点
| 知识点 | 说明 |
|---|---|
| Stage 模型 | 鸿蒙推荐的 Ability 架构,模块化清晰 |
| main_pages.json 路由注册 | 所有页面必须在此注册 |
| $r 资源引用 | 颜色/字号/字符串统一管理 |
| @Component + @State | 声明式 UI 与状态驱动 |
| @Builder | UI 组件复用 |
| ForEach | 列表渲染与 key 优化 |
| router.pushUrl | 跨页面导航 |
避坑指南
- 路由路径不加
.ets后缀:router.pushUrl({ url: 'pages/TodoPage' })而非TodoPage.ets - 对象字面量必须显式声明类型:ArkTS 严格模式下不允许推断对象类型,所有数组元素必须标注接口
@State只监听一层深度的变化:数组内元素的属性变化不会被追踪,需要用this.todos = [...this.todos]触发刷新- 资源引用用
$r('app.xxx.xxx'):不要硬编码颜色/字号值
八、下篇预告
下一篇中,我们将深入开发**「待办事项」页面**,涵盖:
- 分类标签筛选的实现
- 待办 CRUD(增删改查)完整流程
- 状态管理的最佳实践
- 弹窗对话框的设计与交互
欢迎持续关注,一起掌握鸿蒙原生应用开发!
