简介:基于HarmonyOS 4的新闻类App源代码"hongmeng-headlines",面向初入鸿蒙生态的移动端开发者,旨在以真实项目串联UI搭建、数据通信与设备协同等核心技能。项目基于DevEco Studio构建,包含完整的新闻列表与文章详情界面,适合系统学习鸿蒙应用从零到一的工程实践,也可作为课程设计或毕业设计的参考。压缩包内共78个文件,以ets界面逻辑、json5/json配置为主,辅以svg/png图标、ts构建脚本及tgz依赖,整体仅22.75MB,目录按模块划分,便于逐段研读。目前已有1447人学习下载。通过源码可掌握组件化布局、界面设计、网络请求与JSON解析、本地数据存储、事件监听、生命周期管理、多设备适配、权限申请乃至分布式协同等关键知识点;同时项目提供了清晰的基础模板,便于在此基础上扩展推荐、搜索等个性化新闻功能,兼具教学与二次开发价值。
1. 为什么是 HarmonyOS 4:新闻类 APP 的生态卡位与技术起点
新闻类 APP 是移动端最普及、也最容易写僵的品类,但如果在 HarmonyOS 4 上从零搭一套可编译的源代码,反而比聊天、支付这类应用更轻——不碰复杂传感器、不依赖私有系统服务,核心链路就是网络请求、列表渲染、Web 详情页三条线。下面顺着这三条线,以 API 10 为基线,用 ArkTS 串成一个最小可运行工程:先定 Stage 模型下的模块边界,再解决数据从哪来、列表怎么不卡、详情页怎么承接,最后走到上架前的调试与自检。适合准备过 HarmonyOS 应用基础认证、想直接看代码落点的开发者,也适合评估新闻类 App 迁移成本的客户端团队。
2. HarmonyOS 4 新闻 APP 的代码骨架:Stage 模型、模块划分与数据流设计
拿到需求先别急着写 List。新闻类 App 会在冷启动、切后台、首屏加载三个场景被用户反复考验,这决定了代码骨架先于功能细节。HarmonyOS 4 把所有应用入口收敛到 Stage 模型,不少从 FA 模型迁过来的工程会在这一步返工。
2.1 为什么 Stage 模型是新闻类 App 的默认起点
Stage 模型把应用入口拆成 UIAbility 与 ExtensionAbility 两类能力。新闻客户端只需要一个主界面入口,用 UIAbility 就够,启动方式配成 singleton,避免用户从桌面图标、最近任务、推送通知三个入口进来时拉起多份页面栈。module.json5 里和新闻场景直接相关的四个字段如下,改动之前先读一遍。
{ "module": { "name": "entry", "type": "entry", "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "launchType": "singleton", "skills": [ { "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ] } }launchType 默认是 singleton,不建议为了“多窗口”改成 multiton。新闻场景没有多开诉求,multiton 会让每次点击图标都新建一个 Ability,返回键行为变得混乱。skills 里保留 Home 入口即可,推送通知的跳转在 onNewWant 回调里做二次分发,不需要注册多条 skill。
冷启动路径上还有一件事:EntryAbility 的 onWindowStageCreate 里只做窗口加载,把网络服务这类全局依赖放到 Ability 的成员变量里初始化,不要放在页面组件内部。页面组件随路由销毁重建,而网络层希望只存在一份。
2.2 新闻模块的工程目录与最小代码结构
entry/src/main/ets/ ├── entryability/EntryAbility.ets ├── pages/ │ ├── Index.ets │ └── NewsDetail.ets ├── model/ │ └── NewsModels.ets ├── network/ │ ├── NewsService.ets │ └── HttpWrapper.ets ├── components/ │ ├── NewsList.ets │ └── NewsListItem.ets └── common/ └── Constants.ets这个划分刻意不引入 router 层。页面少时,路由跳转直接用页面路由的 pushUrl 按字符串路径管理,等页面超过五个再抽象路由表。model 只放类型定义和序列化方法,network 只负责请求和解析,components 里的 NewsListItem 接收一个 NewsItem 对象,不直接触碰网络层。
| 目录 | 职责 | 变更频率 |
|---|---|---|
| model | 定义 NewsItem、NewsChannel、分页结构 | 低 |
| network | 请求、超时、重试、错误归一 | 中 |
| components | 列表卡片、骨架屏、图片占位 | 高 |
| pages | 组装页面状态与生命周期 | 中 |
变更频率最高的是 components,信息流的卡片样式迭代基本都集中在这里。model 和 network 要尽量稳定,接口响应结构变了,顶多改改 model 字段,UI 层不动。
2.3 数据流设计:从接口到 ListItem 的状态流转
常见做法是给列表页做一个 ViewModel 容器,把加载中、加载成功、加载失败、加载更多四个状态收拢到一个可观察对象里。这个容器不持有 ArkUI 组件,只持有业务数据,页面通过 @State 持有容器的引用。
class NewsFeedState { items: NewsItem[] = [] isLoading: boolean = false errorMsg: string = '' page: number = 1 hasMore: boolean = true }页面在 onPageShow 里触发首次加载,加载完成后整体赋值 items。不要在 NewsListItem 里各自发请求,否则快速滑动时请求数量会失控。分页页码就放在容器里,滑动到底部时只做 page+1 的增量请求,新数据追加到 items 尾部。
ViewModel 别放进自定义组件。组件销毁后数据跟着清空,从详情页返回列表时又会重新拉接口,新闻流用户一天要进出详情页几十次,重复请求量很可观。把容器放到 EntryAbility 的应用级单例里,或者用 AppStorage 做跨页面缓存,都比绑定在页面生命周期上可控。
3. 拿到新闻数据的三种方式:RSS 解析、JSON 接口与本地 Mock 数据
新闻类 APP 开发早期最大的变量不是 UI 而是数据源。标题里“源代码”三个字让人以为核心是界面代码,实际上一个能演示的新闻 App,八成时间在跟数据格式打交道。三种数据源最常用,按接入成本排个序。
3.1 三种数据源的选型与适用阶段
| 数据源 | 格式 | 适合阶段 | 主要成本 |
|---|---|---|---|
| RSS 2.0 | XML | 快速验证阅读体验 | 字段少、无图片尺寸、编码杂 |
| 开放新闻 API | JSON | 产品原型与内测 | 需要申请 Key、有配额限制 |
| 本地 Mock | JSON 文件 | 开发期与 UI 联调 | 需维护假数据,易与真实接口脱节 |
RSS 内容老一点,但胜在即时可得,找两三个科技或财经博客的 feed,先用真数据把排版调好,再去接开放 API。不建议一上来就接大厂的新闻接口,申请流程和限流规则会拖慢首屏联调。RSS 本质是 XML,ArkTS 里用 @ohos.convertxml 的 ConvertXML 类把 feed 转成对象,再取 title、description、pubDate 字段,够搭一个阅读器原型。商用场景不推荐 RSS,很多站点在图片上做防盗链,列表页会缺图。
本地 Mock 经常被忽略。写一个挂在 assets 下的 news-mock.json,字段结构和真实接口保持一致,前端先把加载态、空态、失败态全部走通,后端接口联调时才不会互相等。
3.2 用 ArkTS 解析 JSON 新闻数据的最小实现
开放新闻 API 通常返回固定结构:错误码、数据列表、分页信息。先把响应体声明成类型,再解析,这一步不要偷懒。
export interface NewsItem { id: number title: string summary: string source: string publishTime: string imageUrl: string contentUrl: string } export interface NewsResponse { code: number message: string data: { list: NewsItem[] hasMore: boolean nextPage: number } }然后用 HttpWrapper 封装 @ohos.net.http。下面这段是请求函数的核心,超时时间、请求头、错误处理都集中在这里:
import http from '@ohos.net.http' export class NewsService { async fetchNews(page: number): Promise<NewsItem[]> { const httpRequest = http.createHttp() try { const resp = await httpRequest.request( `${Constants.BASE_URL}/news?page=${page}&size=20`, { method: http.RequestMethod.GET, connectTimeout: 10000, readTimeout: 10000, header: { 'Content-Type': 'application/json' } } ) const body = JSON.parse(resp.result as string) as NewsResponse if (body.code !== 0) throw new Error(`api error ${body.code}`) return body.data.list } finally { httpRequest.destroy() } } }三个参数值得细说。第一,createHttp 创建的实例用完必须 destroy,复用全局实例在长时间运行后可能遇到连接无响应,日志里表现为 promise 一直 pending。第二,connectTimeout 与 readTimeout 在新闻场景建议都设 10 秒,首屏数据超过 3 秒用户就流失,但把超时压到 3 秒又会在弱网下频繁失败,客户端做一次重试并记录第一次失败原因,比盲目缩短超时有效。第三,resp.result 默认是字符串,务必先 JSON.parse 再断言成 NewsResponse,直接强转会在字段缺失时拿到 undefined,后续列表渲染直接崩。
3.3 配置网络权限与明文流量:跑真机前必做的两处修改
真机跑新闻 App 第一步遇到请求报 2300006 错误码,多半是网络权限没给。HarmonyOS 4 在 module.json5 里声明权限,不在页面代码里做动态申请。
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ], "metadata": [ { "name": "network_security_config", "resource": "$profile:network_config" } ] } }如果接口地址是 http 明文,还需要放行明文流量。在 resources/base/profile/network_config.xml 里写:
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true"> <trust-anchors> <certificates src="system" /> </trust-anchors> </base-config> </network-security-config>metadata 里 network_security_config 这个 key 是固定的,resource 指向 profile 目录下的 xml 文件名,不要写到 rawfile。正式切 https 后,这段配置可以整段拿掉。调试时加载 DevEco 的 Log 面板,过滤 ohos.net.http 能看到每个请求的失败行,2300006 在权限缺失时出现频率最高,其次是设备网络不可达,按日志顺序排查比盲改代码快。真机请求报证书校验失败时,先看系统时间是否准确,证书校验在时间跳变的设备上会先失败。
4. ArkUI 渲染新闻列表与详情页:List、Web 组件与图片缓存
列表渲染是新闻类 APP 体验的分水岭。方案一:Scroll 加 ForEach 硬渲染,数据量小的时候没问题,新闻流数据几百条起步,内存上涨明显。方案二:直接用 List 加 LazyForEach,只渲染可视区域,这是 HarmonyOS 4 下新闻列表的默认解法。
4.1 新闻列表用 LazyForEach 实现懒加载
先写一个实现 IDataSource 的数据源类,它负责告诉 List 总共多少条、每条数据是什么,以及在数据追加时发通知。
class NewsDataSource implements IDataSource { private items: NewsItem[] = [] private listeners: DataChangeListener[] = [] totalCount(): number { return this.items.length } getData(index: number): NewsItem { return this.items[index] } registerDataChangeListener(listener: DataChangeListener): void { this.listeners.push(listener) } unregisterDataChangeListener(listener: DataChangeListener): void { const idx = this.listeners.indexOf(listener) if (idx >= 0) this.listeners.splice(idx, 1) } addItems(newItems: NewsItem[]): void { const start = this.items.length this.items.push(...newItems) this.listeners.forEach(l => l.onDataAdd(start, newItems.length)) } }这个类把新增数据的通知封装成 onDataAdd,让 LazyForEach 增量刷新而不是全量重建。页面里这样组装 List:
List({ space: 12, scroller: this.scroller }) { LazyForEach(this.dataSource, (item: NewsItem) => { ListItem() { NewsListItem({ item: item }) } }, (item: NewsItem) => `${item.id}_${item.publishTime}`) } .cachedCount(4) .onReachEnd(() => this.loadMore())LazyForEach 的第三个参数是键值生成函数,必须能区分每条数据。用索引当 key 会导致插入或删除时复用错乱,新闻流常有置顶、移除操作,所以用 id 加时间戳组合。cachedCount(4) 表示首屏方向多缓存 4 条,减少回滑白屏;调到 10 不会让内存显著上涨,但再大就失去懒加载意义了,滑出屏幕的图片迟迟不释放。onReachEnd 在滚动到底部时触发分页。
4.2 新闻详情页用 Web 组件承载富文本
新闻详情最常见的数据形态是服务端下发的 HTML 片段或整页链接。原生解析 HTML 成本高,图片、排版、字体都难还原,详情页用 Web 组件加载 contentUrl 是行业统一做法。
Web({ src: this.contentUrl, controller: this.controller }) .domStorageAccess(true) .javaScriptAccess(true) .onErrorReceive((event) => { this.errorVisible = true }) .onHttpErrorReceive((event) => { console.error(`http error: ${event.response.getResponseCode()}`) })两个点要注意。第一,domStorageAccess 要开,部分新闻站点的懒加载脚本依赖 localStorage。第二, Web 页里用户滚动很多屏后返回,再次进入会重建并回到顶部,这是正常行为,不需要做滚动位置恢复,否则内存维护会变复杂。
阅读字号是新闻 App 的高频设置项,但设置方式要区分场景:列表页的字号绑一个 @State 驱动 Text 组件没问题,详情页的字号要等页面加载完成后注入给 H5。
this.controller.runJavaScript(` document.body.style.fontSize = '${this.fontSize}px'; `)runJavaScript 必须在 onPageEnd 回调之后再调用,页面没加载完会报错。有些站点正文不是直接挂在 body 下,需要再查一下文章容器的选择器,这里只提供一个通用注入入口。
4.3 图片加载:占位图与内存控制
列表卡片一般都有缩略图。Image 组件的加载、解码和缓存由系统接管,开发阶段不需要引入三方图片库,但两个参数要设置到位:
Image(item.imageUrl) .width(120) .height(80) .objectFit(ImageFit.Cover) .alt($r('app.media.placeholder')) .onError(() => { this.showErrorPlaceholder = true })| 参数 | 作用 | 建议值 |
|---|---|---|
| objectFit | 图片缩放模式 | Cover,避免变形 |
| alt | 加载中与失败占位图 | 本地资源 |
| onError | 加载失败回调 | 切本地图或隐藏 |
不要用 ImageFit.Contain,新闻缩略图是固定宽高比裁剪,Contain 会留白,卡片视觉上参差。图片 URL 的域名有条件的话加进 network_config 的 domain-config,省一次跳转等待。列表页面的加载更多状态建议单独放一个 footer 组件,滚动到底部时显示转圈,避免用弹窗打断滑动。
5. 从源代码到上架:抓包排查、性能数据与发布前自检
代码写完之后,从源代码到可发布版本之间还有一段路。这一章把调试、性能和上架前必须做的手续集中梳理一遍,都是可复现的操作。
5.1 新闻 App 抓包失败的三个排查点
开发期抓包失败是最常见的求助场景。出现抓不到新闻请求时,按下面顺序查,能省下大量排查时间:
- 设备与抓包工具是否在同一网络。不在同一网段时流量根本过不去,这是第一排查点。
- 抓包证书是否装进系统信任区。只装用户证书时,部分新闻接口在 HTTP/2 下会保持连接不触发抓包。
- 目标接口是否为 HTTPS。证书没配对前只能看到连接握手,看不到 HTTP 层内容。
排查完这三项还抓不到,把 App 完全杀掉重开。新闻类接口不少在启动后几秒内并发请求,抓包工具的过滤条件设得过窄时容易漏掉。抓包的目的不是截取数据,而是确认请求头、响应体和缓存策略是否符合预期。
5.2 用 DevEco Profiler 看列表性能
信息流卡顿不要凭感觉。DevEco Studio 的 Profiler 面板可以抓 ArkUI 渲染帧耗时。打开 Profiler 后操作列表快速滑动,观察 Frame 和 Layout 两个指标。
| 指标 | 关注点 | 合格线 |
|---|---|---|
| Frame | 平均帧耗时 | 低于 16ms |
| Layout | 列表项布局时间 | 单帧不超过 8ms |
| Memory | 图片与 Web 缓存占用 | 占用不持续上涨 |
如果 Layout 超时,优先检查 NewsListItem 里是否有不必要的 @State。放在子组件里但从不改动的字段,不要用 @State 修饰,多余的装饰器会让整棵子树参与 diff。Profiler 里能看到每个自定义组件的创建耗时,卡片创建超过 3ms 就该拆静态模板了。
5.3 源码版本管理与发布前自检
源代码与原始版本脱钩是二次修改的前提。调试用的 BASE_URL 不能进发布包,由构建脚本替换,或者维护一个单独的常量文件,发布前只动这一处:
// common/ProfileConfig.ets export class ProfileConfig { static readonly BASE_URL: string = 'https://debug.news.example.com' // 发布前切换为 release 地址,并由构建脚本注入 }调试包和发布包不要维护两套代码,否则容易在发版时选错配置。发布前再走一遍这六项:权限只留 INTERNET,不开存储与位置权限;隐私政策里写清采集哪些字段;深色模式下列表卡片对比度足够;断网时有错误提示而不是白屏;连续启动退出三次无崩溃;通过 HarmonyOS 应用基础认证后再提交审核。最后在 DevEco 里打 release 包,用正式 keystore 签名,勾选混淆开关。提交前的动作是卸载设备上的调试签名旧包再装正式包,签名冲突会导致安装失败,登录态和推送通道各跑一遍,确认后走发布流程。
本文还有配套的精品资源,点击获取