鸿蒙完整项目实战(下):新闻资讯App的收藏功能+设置页+App打包上架全流程
鸿蒙NEXT开发实战系列| 第37篇 | 实战篇 适合人群完成上篇学习的开发者 ⏰阅读时间约20分钟 | 开发环境DevEco Studio 5.0 目录导航一、前言二、收藏功能实现RDB数据库2.1 RDB数据库初始化2.2 收藏数据模型定义2.3 收藏功能Service层实现2.4 收藏页面UI实现三、设置页与主题切换3.1 设置页布局实现3.2 深色/浅色模式切换3.3 主题持久化存储四、HAP打包与签名4.1 生成签名文件4.2 配置签名信息4.3 打包HAP文件五、应用市场上架流程5.1 注册开发者账号5.2 创建应用5.3 上传与审核六、项目总结与优化建议七、系列文章推荐一、前言在上篇中我们完成了新闻资讯App的基础功能开发包括首页新闻列表、新闻详情页、网络请求封装等核心模块。本篇作为下篇将继续完善App的剩余功能收藏功能使用RDB关系数据库实现新闻收藏的增删查操作设置页面实现个人中心与深色/浅色主题切换打包上架详细介绍HAP包签名、打包及应用市场上架全流程完成本篇学习后你将拥有一个功能完整、可发布上线的鸿蒙原生应用项目源码结构回顾NewsApp/ ├── entry/src/main/ets/ │ ├── common/ // 公共组件 │ ├── database/ // 数据库操作本篇新增 │ ├── model/ // 数据模型 │ ├── pages/ // 页面 │ │ ├── Index.ets // 首页 │ │ ├── Detail.ets // 详情页 │ │ ├── Favorite.ets // 收藏页本篇新增 │ │ └── Setting.ets // 设置页本篇新增 │ └── service/ // 业务逻辑 └── entry/src/main/resources/二、收藏功能实现RDB数据库2.1 RDB数据库初始化鸿蒙提供了ohos.data.relationalStore模块用于关系型数据库操作。首先创建数据库管理类// database/RdbHelper.ets import relationalStore from ohos.data.relationalStore; import { FavoriteNews } from ../model/FavoriteNews; const DB_NAME NewsApp.db; const DB_VERSION 1; const TABLE_FAVORITE favorite_news; export class RdbHelper { private store: relationalStore.RdbStore | null null; // 初始化数据库 async initRdbStore(context: Context): Promisevoid { const STORE_CONFIG: relationalStore.StoreConfig { name: DB_NAME, securityLevel: relationalStore.SecurityLevel.S1 }; try { this.store await relationalStore.getRdbStore(context, STORE_CONFIG); await this.createTable(); console.info([RdbHelper] 数据库初始化成功); } catch (err) { console.error([RdbHelper] 数据库初始化失败: ${err}); } } // 创建收藏表 private async createTable(): Promisevoid { const createSql CREATE TABLE IF NOT EXISTS ${TABLE_FAVORITE} ( id INTEGER PRIMARY KEY AUTOINCREMENT, news_id TEXT NOT NULL UNIQUE, title TEXT NOT NULL, image_url TEXT, source TEXT, publish_time TEXT, create_time INTEGER NOT NULL ) ; await this.store?.executeSql(createSql); } // 插入收藏 async insertFavorite(news: FavoriteNews): Promisenumber { const bucket: relationalStore.ValuesBucket { news_id: news.newsId, title: news.title, image_url: news.imageUrl, source: news.source, publish_time: news.publishTime, create_time: Date.now() }; const predicates new relationalStore.RdbPredicates(TABLE_FAVORITE); const resultSet await this.store!.query(predicates, [news_id]); // 检查是否已收藏 while (resultSet.goToNextRow()) { const existingId resultSet.getString(resultSet.getColumnIndex(news_id)); if (existingId news.newsId) { resultSet.close(); console.info([RdbHelper] 该新闻已收藏); return -1; } } resultSet.close(); const rowId await this.store!.insert(TABLE_FAVORITE, bucket); console.info([RdbHelper] 收藏成功, rowId: ${rowId}); return rowId; } // 删除收藏 async deleteFavorite(newsId: string): Promisenumber { const predicates new relationalStore.RdbPredicates(TABLE_FAVORITE); predicates.equalTo(news_id, newsId); const rowCount await this.store!.delete(predicates); console.info([RdbHelper] 取消收藏成功, 删除行数: ${rowCount}); return rowCount; } // 查询所有收藏 async queryAllFavorites(): PromiseFavoriteNews[] { const predicates new relationalStore.RdbPredicates(TABLE_FAVORITE); predicates.orderByDesc(create_time); const resultSet await this.store!.query(predicates, [ id, news_id, title, image_url, source, publish_time, create_time ]); const favorites: FavoriteNews[] []; while (resultSet.goToNextRow()) { favorites.push({ newsId: resultSet.getString(resultSet.getColumnIndex(news_id)), title: resultSet.getString(resultSet.getColumnIndex(title)), imageUrl: resultSet.getString(resultSet.getColumnIndex(image_url)), source: resultSet.getString(resultSet.getColumnIndex(source)), publishTime: resultSet.getString(resultSet.getColumnIndex(publish_time)) }); } resultSet.close(); return favorites; } // 检查是否已收藏 async isFavorite(newsId: string): Promiseboolean { const predicates new relationalStore.RdbPredicates(TABLE_FAVORITE); predicates.equalTo(news_id, newsId); const resultSet await this.store!.query(predicates, [news_id]); const count resultSet.rowCount; resultSet.close(); return count 0; } } // 全局单例 export const rdbHelper new RdbHelper();2.2 收藏数据模型定义// model/FavoriteNews.ets export interface FavoriteNews { newsId: string; // 新闻ID title: string; // 标题 imageUrl?: string; // 封面图 source?: string; // 来源 publishTime?: string; // 发布时间 }2.3 收藏功能Service层实现// service/FavoriteService.ets import { rdbHelper } from ../database/RdbHelper; import { FavoriteNews } from ../model/FavoriteNews; export class FavoriteService { // 切换收藏状态 static async toggleFavorite(news: FavoriteNews): Promiseboolean { const isFav await rdbHelper.isFavorite(news.newsId); if (isFav) { await rdbHelper.deleteFavorite(news.newsId); return false; // 取消收藏 } else { await rdbHelper.insertFavorite(news); return true; // 收藏成功 } } // 获取所有收藏 static async getAllFavorites(): PromiseFavoriteNews[] { return await rdbHelper.queryAllFavorites(); } // 检查收藏状态 static async checkIsFavorite(newsId: string): Promiseboolean { return await rdbHelper.isFavorite(newsId); } }2.4 收藏页面UI实现// pages/Favorite.ets import { FavoriteService } from ../service/FavoriteService; import { FavoriteNews } from ../model/FavoriteNews; import router from ohos.router; Entry Component struct FavoritePage { State favoriteList: FavoriteNews[] []; State isLoading: boolean true; async aboutToAppear(): Promisevoid { await this.loadFavorites(); } async loadFavorites(): Promisevoid { this.isLoading true; this.favoriteList await FavoriteService.getAllFavorites(); this.isLoading false; } build() { Column() { // 标题栏 Row() { Image($r(app.media.ic_back)) .width(24) .height(24) .onClick(() router.back()) Text(我的收藏) .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ left: 12 }) } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .alignItems(VerticalAlign.Center) // 收藏列表 if (this.isLoading) { LoadingProgress() .width(48) .height(48) } else if (this.favoriteList.length 0) { // 空状态 Column() { Image($r(app.media.ic_empty)) .width(120) .height(120) .margin({ bottom: 16 }) Text(暂无收藏) .fontSize(16) .fontColor(#999999) } .justifyContent(FlexAlign.Center) .layoutWeight(1) } else { // 收藏列表 List({ space: 12 }) { ForEach(this.favoriteList, (item: FavoriteNews) { ListItem() { Row() { Column() { Text(item.title) .fontSize(16) .fontWeight(FontWeight.Medium) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text(item.source || ) .fontSize(12) .fontColor(#999999) Text(item.publishTime || ) .fontSize(12) .fontColor(#999999) .margin({ left: 12 }) } .margin({ top: 8 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) // 取消收藏按钮 Image($r(app.media.ic_favorite_filled)) .width(24) .height(24) .fillColor(#FF4081) .onClick(async () { await FavoriteService.toggleFavorite(item); await this.loadFavorites(); }) } .padding(16) .backgroundColor(#FFFFFF) .borderRadius(12) } }) } .padding({ left: 16, right: 16, top: 12 }) .layoutWeight(1) } } .width(100%) .height(100%) .backgroundColor(#F5F5F5) } }三、设置页与主题切换3.1 设置页布局实现// pages/Setting.ets import { PreferencesHelper } from ../common/PreferencesHelper; import router from ohos.router; Entry Component struct SettingPage { State isDarkMode: boolean false; State fontSize: number 14; State clearCacheSuccess: boolean false; async aboutToAppear(): Promisevoid { // 读取主题设置 const theme await PreferencesHelper.get(theme_mode, light); this.isDarkMode theme dark; } build() { Column() { // 标题栏 Row() { Image($r(app.media.ic_back)) .width(24) .height(24) .onClick(() router.back()) Text(设置) .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ left: 12 }) } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .alignItems(VerticalAlign.Center) Scroll() { Column({ space: 12 }) { // 主题设置区域 Column() { Text(显示设置) .fontSize(14) .fontColor(#999999) .margin({ bottom: 8 }) // 深色模式开关 Row() { Row() { Image($r(app.media.ic_dark_mode)) .width(24) .height(24) .margin({ right: 12 }) Text(深色模式) .fontSize(16) } Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode }) .onChange(async (isOn: boolean) { this.isDarkMode isOn; await PreferencesHelper.set(theme_mode, isOn ? dark : light); AppStorage.setOrCreateboolean(isDarkMode, isOn); }) } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .justifyContent(FlexAlign.SpaceBetween) Divider() // 字体大小 Row() { Row() { Image($r(app.media.ic_font_size)) .width(24) .height(24) .margin({ right: 12 }) Text(字体大小) .fontSize(16) } Row({ space: 16 }) { Button(-) .width(32) .height(32) .fontSize(18) .onClick(() { if (this.fontSize 12) this.fontSize--; }) Text(${this.fontSize}px) .fontSize(16) Button() .width(32) .height(32) .fontSize(18) .onClick(() { if (this.fontSize 20) this.fontSize; }) } } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .justifyContent(FlexAlign.SpaceBetween) } .backgroundColor(#FFFFFF) .borderRadius(12) .padding({ top: 16, bottom: 8 }) // 数据管理区域 Column() { Text(数据管理) .fontSize(14) .fontColor(#999999) .margin({ bottom: 8 }) Row() { Row() { Image($r(app.media.ic_clear_cache)) .width(24) .height(24) .margin({ right: 12 }) Text(清除缓存) .fontSize(16) } Row({ space: 4 }) { if (this.clearCacheSuccess) { Text(已清除) .fontSize(14) .fontColor(#4CAF50) } else { Text(12.5MB) .fontSize(14) .fontColor(#999999) } Image($r(app.media.ic_arrow_right)) .width(16) .height(16) } } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .justifyContent(FlexAlign.SpaceBetween) .onClick(() { this.clearCacheSuccess true; }) Divider() // 关于 Row() { Row() { Image($r(app.media.ic_about)) .width(24) .height(24) .margin({ right: 12 }) Text(关于) .fontSize(16) } Row({ space: 4 }) { Text(v1.0.0) .fontSize(14) .fontColor(#999999) Image($r(app.media.ic_arrow_right)) .width(16) .height(16) } } .width(100%) .height(56) .padding({ left: 16, right: 16 }) .justifyContent(FlexAlign.SpaceBetween) } .backgroundColor(#FFFFFF) .borderRadius(12) .padding({ top: 16, bottom: 8 }) } .padding(16) } .layoutWeight(1) } .width(100%) .height(100%) .backgroundColor(#F5F5F5) } }3.2 深色/浅色模式切换在AbilityStage中全局监听主题变化// entryability/EntryAbility.ets import UIAbility from ohos.app.ability.UIAbility; import { PreferencesHelper } from ../common/PreferencesHelper; export default class EntryAbility extends UIAbility { async onCreate(): Promisevoid { // 读取保存的主题设置 const theme await PreferencesHelper.get(theme_mode, light); AppStorage.setOrCreateboolean(isDarkMode, theme dark); } }在页面中使用主题配置// common/ThemeConfig.ets export class ThemeConfig { // 浅色主题 static light { backgroundColor: #F5F5F5, cardColor: #FFFFFF, textColor: #333333, subTextColor: #999999, primaryColor: #1890FF, statusBarContentColor: dark }; // 深色主题 static dark { backgroundColor: #1A1A1A, cardColor: #2D2D2D, textColor: #FFFFFF, subTextColor: #999999, primaryColor: #40A9FF, statusBarContentColor: light }; // 获取当前主题 static getCurrentTheme(isDark: boolean) { return isDark ? ThemeConfig.dark : ThemeConfig.light; } }3.3 主题持久化存储// common/PreferencesHelper.ets import dataPreferences from ohos.data.preferences; const PREFERENCES_NAME NewsAppPrefs; export class PreferencesHelper { private static preferences: dataPreferences.Preferences | null null; static async init(context: Context): Promisevoid { PreferencesHelper.preferences await dataPreferences.getPreferences(context, PREFERENCES_NAME); } static async get(key: string, defaultValue: string): Promisestring { if (!PreferencesHelper.preferences) return defaultValue; const value await PreferencesHelper.preferences.get(key, defaultValue); return value as string; } static async set(key: string, value: string): Promisevoid { if (!PreferencesHelper.preferences) return; await PreferencesHelper.preferences.put(key, value); await PreferencesHelper.preferences.flush(); } }四、HAP打包与签名4.1 生成签名文件在打包之前需要先生成应用签名文件打开DevEco Studio点击菜单Build Generate Key and CSR配置Key信息Key File Path选择签名文件保存路径如D:\keys\news_app.p12Password设置密钥密码建议使用强密码Key Alias设置别名如news_app_keyValidity有效期默认25年CN/OU/O等信息按实际填写生成CSR文件选择CSR文件保存路径点击Next完成生成4.2 配置签名信息打开File Project Structure Signing Configs配置签名信息// module.json5 中的签名配置 { app: { signingConfigs: [ { name: release, type: HarmonyOS, material: { certpath: D:\\keys\\news_app.cer, storePassword: ******, keyAlias: news_app_key, keyPassword: ******, profile: D:\\keys\\news_app_debug.p7b, signAlg: SHA256withECDSA, storeFile: D:\\keys\\news_app.p12 } } ] } }注意正式发布时需要使用正式证书.p7b文件从AppGallery Connect下载4.3 打包HAP文件步骤一选择Build模式在DevEco Studio顶部工具栏选择release构建模式。步骤二执行打包Build Build Hap(s)/APP(s) Build Hap(s)步骤三获取产物打包成功后在entry/build/default/outputs/目录下可找到.hap文件。步骤四验证HAP使用命令行工具验证HAP包完整性# 检查HAP包信息 hap-tool check entry-default-signed.hap五、应用市场上架流程5.1 注册开发者账号访问 AppGallery Connect使用华为账号登录如无账号需先注册完成开发者实名认证个人开发者身份证认证企业开发者企业资质认证5.2 创建应用进入 AppGallery Connect 我的项目点击添加应用应用类型鸿蒙应用应用名称填写App名称如今日资讯应用分类选择新闻阅读默认语言简体中文完成创建后进入应用管理页面5.3 上传与审核步骤一准备应用资料资料项要求应用图标216x216pxPNG格式应用截图至少3张1080x1920px应用描述100-4000字介绍App功能特色隐私政策必须提供有效的隐私政策URL软件著作权如有可加速审核步骤二上传HAP包进入版本管理软件包管理点击上传选择打包好的.hap文件等待上传完成系统自动校验步骤三填写版本信息版本号与module.json5中的versionCode一致更新说明描述本次更新内容步骤四提交审核检查所有信息填写完整点击提交审核审核周期一般为1-3个工作日审核常见问题问题类型解决方案隐私政策不合规确保隐私政策URL可访问内容完整应用图标不规范按要求调整尺寸和格式功能描述不清晰详细描述App核心功能存在崩溃问题提前做好充分测试六、项目总结与优化建议通过本系列的学习我们完成了一个完整的鸿蒙新闻App开发已实现功能新闻列表展示与下拉刷新新闻详情页WebView加载新闻收藏功能RDB数据库设置页面与主题切换HAP打包与上架流程优化建议性能优化使用LazyForEach实现列表懒加载图片加载增加缓存机制网络请求增加重试机制功能增强增加新闻搜索功能实现新闻分类标签增加评论互动功能接入推送服务代码质量引入状态管理框架如V2版本状态管理完善错误处理和边界情况增加单元测试覆盖七、系列文章推荐序号文章标题内容概要35鸿蒙完整项目实战上新闻App首页与详情页项目搭建、首页列表、详情页开发36ArkUI进阶复杂列表布局与性能优化高级布局技巧、性能调优37本文收藏功能、设置页、打包上架 相关资源鸿蒙官方文档 - RDB数据库鸿蒙官方文档 - 应用打包AppGallery Connect帮助中心标签鸿蒙上架HAP打包AppGallery鸿蒙实战完整项目RDB数据库主题切换设置页开发系列鸿蒙NEXT开发实战系列 | 第37篇下篇预告我们将开始新的实战项目——鸿蒙智能记账App深入学习数据可视化图表、AI能力集成等进阶内容。敬请期待