鸿蒙完整项目实战(上):从零搭建一个新闻资讯App,架构设计+首页+详情页
本文是鸿蒙NEXT开发实战系列第36篇,指导开发者完成新闻资讯App的核心功能开发。文章采用分层架构设计,包含UI层、ViewModel层、Service层和Network层。主要内容包括:1) 基于axios封装网络请求工具类;2) 实现首页新闻列表功能,支持下拉刷新和上拉加载;3) 开发新闻详情页,使用WebView展示内容;4) 配置页面路由。文章提供了完整的代码示例和架构图示,适合已完成鸿
📖 鸿蒙NEXT开发实战系列 | 第36篇 | 实战篇 🎯 适合人群:完成鸿蒙基础学习的开发者 ⏰ 阅读时间:约20分钟 | 💻 开发环境:DevEco Studio 5.0+
简介:学了这么多知识点,该做个完整项目了!本系列上篇带你搭建一个新闻资讯App的完整架构,实现首页新闻列表(下拉刷新+上拉加载)和新闻详情页,涵盖网络请求、数据解析、页面路由全链路。
导航链接
目录
一、项目概述与功能规划
在学习了鸿蒙开发的基础知识后,我们需要一个完整的实战项目来串联所学内容。本教程将带你从零搭建一个新闻资讯App,实现以下核心功能:
功能模块
|
模块 |
功能点 |
说明 |
|---|---|---|
|
首页 |
新闻列表 |
使用List组件展示新闻卡片 |
|
首页 |
下拉刷新 |
Refresh组件实现下拉刷新 |
|
首页 |
上拉加载 |
List滑动到底部触发加载更多 |
|
详情页 |
富文本展示 |
WebView加载新闻详情 |
|
网络 |
请求封装 |
基于axios封装统一请求模块 |
技术栈选择
-
UI框架:ArkUI声明式开发
-
网络请求:@ohos/axios
-
数据管理:AppStorage + 自定义状态管理
-
路由导航:Navigation + NavRouter
二、项目架构设计
良好的架构是项目成功的基础。我们采用分层架构设计,让代码结构清晰、易于维护。
目录结构
NewsApp/
├── entry/src/main/ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # 应用入口
│ ├── pages/
│ │ ├── Index.ets # 主页面(使用Tabs)
│ │ ├── HomeTab.ets # 首页Tab内容
│ │ └── DetailPage.ets # 新闻详情页
│ ├── components/
│ │ ├── NewsCard.ets # 新闻卡片组件
│ │ ├── NewsList.ets # 新闻列表组件
│ │ └── EmptyView.ets # 空状态视图
│ ├── model/
│ │ └── NewsModel.ets # 新闻数据模型
│ ├── network/
│ │ ├── HttpUtils.ets # 网络请求工具类
│ │ └── ApiConfig.ets # API配置
│ ├── services/
│ │ └── NewsService.ets # 新闻业务服务
│ ├── viewmodel/
│ │ └── HomeViewModel.ets # 首页ViewModel
│ └── utils/
│ ├── DateUtils.ets # 日期工具
│ └── Constants.ets # 常量定义
└── module.json5
架构图示
┌─────────────────────────────────────────────────────┐
│ UI层 (Pages) │
│ Index.ets → HomeTab.ets → NewsList.ets │
│ ↓ │
│ NewsCard.ets │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ ViewModel层 │
│ HomeViewModel.ets │
│ (状态管理 + 业务逻辑处理) │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ Service层 │
│ NewsService.ets │
│ (业务服务封装) │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ Network层 │
│ HttpUtils.ets + ApiConfig.ets │
│ (网络请求封装) │
└─────────────────────────────────────────────────────┘
数据模型定义
首先定义新闻数据模型,对应API返回的数据结构:
// NewsModel.ets
/**
* 新闻数据模型
* 对应API返回的新闻数据结构
*/
export interface NewsItem {
id: string; // 新闻ID
title: string; // 新闻标题
summary: string; // 新闻摘要
imageUrl: string; // 封面图片URL
source: string; // 新闻来源
publishTime: string; // 发布时间
category: string; // 新闻分类
url: string; // 详情页URL
readCount: number; // 阅读量
}
/**
* API响应数据结构
*/
export interface ApiResponse<T> {
code: number; // 状态码
message: string; // 提示信息
data: T; // 响应数据
}
/**
* 新闻列表响应
*/
export interface NewsListResponse {
list: NewsItem[]; // 新闻列表
total: number; // 总数
page: number; // 当前页码
pageSize: number; // 每页数量
}
/**
* 新闻分类枚举
*/
export enum NewsCategory {
ALL = 'all', // 全部
TECH = 'tech', // 科技
FINANCE = 'finance', // 财经
SPORTS = 'sports', // 体育
ENTERTAINMENT = 'entertainment' // 娱乐
}
三、网络请求封装
网络请求是App开发的基础能力,我们基于axios封装一个统一的请求工具类。
安装axios
首先在项目根目录执行安装命令:
ohpm install @ohos/axios
API配置
// ApiConfig.ets
/**
* API配置
* 集中管理所有接口地址
*/
export class ApiConfig {
// 基础URL(使用公开的新闻API)
static readonly BASE_URL: string = 'https://api.example.com/v1';
// 新闻列表接口
static readonly NEWS_LIST: string = '/news/list';
// 新闻详情接口
static readonly NEWS_DETAIL: string = '/news/detail';
// 请求超时时间(毫秒)
static readonly TIMEOUT: number = 15000;
// 默认每页数量
static readonly PAGE_SIZE: number = 20;
}
HTTP工具类封装
// HttpUtils.ets
import axios, { AxiosResponse, AxiosError } from '@ohos/axios';
import { ApiConfig } from './ApiConfig';
/**
* HTTP请求工具类
* 封装axios,提供统一的请求方法
*/
export class HttpUtils {
private static instance: axios.AxiosInstance;
/**
* 获取axios实例(单例模式)
*/
private static getInstance(): axios.AxiosInstance {
if (!HttpUtils.instance) {
HttpUtils.instance = axios.create({
baseURL: ApiConfig.BASE_URL,
timeout: ApiConfig.TIMEOUT,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
HttpUtils.instance.interceptors.request.use(
(config) => {
console.info(`[HTTP] 请求: ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error: AxiosError) => {
console.error(`[HTTP] 请求错误: ${error.message}`);
return Promise.reject(error);
}
);
// 响应拦截器
HttpUtils.instance.interceptors.response.use(
(response: AxiosResponse) => {
console.info(`[HTTP] 响应: ${response.status} ${response.config.url}`);
return response;
},
(error: AxiosError) => {
console.error(`[HTTP] 响应错误: ${error.message}`);
return Promise.reject(error);
}
);
}
return HttpUtils.instance;
}
/**
* GET请求
* @param url 请求地址
* @param params 查询参数
* @returns Promise<any>
*/
static async get<T>(url: string, params?: object): Promise<T> {
try {
const response = await HttpUtils.getInstance().get<T>(url, { params });
return response.data;
} catch (error) {
console.error(`[HTTP] GET请求失败: ${url}`, error);
throw error;
}
}
/**
* POST请求
* @param url 请求地址
* @param data 请求体
* @returns Promise<any>
*/
static async post<T>(url: string, data?: object): Promise<T> {
try {
const response = await HttpUtils.getInstance().post<T>(url, data);
return response.data;
} catch (error) {
console.error(`[HTTP] POST请求失败: ${url}`, error);
throw error;
}
}
}
新闻服务层
// NewsService.ets
import { HttpUtils } from '../network/HttpUtils';
import { ApiConfig } from '../network/ApiConfig';
import { NewsItem, ApiResponse, NewsListResponse } from '../model/NewsModel';
/**
* 新闻业务服务
* 封装新闻相关的业务接口
*/
export class NewsService {
/**
* 获取新闻列表
* @param page 页码
* @param pageSize 每页数量
* @param category 新闻分类
* @returns Promise<NewsItem[]>
*/
static async getNewsList(
page: number = 1,
pageSize: number = ApiConfig.PAGE_SIZE,
category: string = 'all'
): Promise<NewsItem[]> {
try {
const response = await HttpUtils.get<ApiResponse<NewsListResponse>>(
ApiConfig.NEWS_LIST,
{ page, pageSize, category }
);
if (response.code === 200) {
return response.data.list;
}
console.error(`获取新闻列表失败: ${response.message}`);
return [];
} catch (error) {
console.error('获取新闻列表异常:', error);
// 返回模拟数据用于开发测试
return NewsService.getMockNewsList(page, pageSize);
}
}
/**
* 获取新闻详情
* @param newsId 新闻ID
* @returns Promise<NewsItem | null>
*/
static async getNewsDetail(newsId: string): Promise<NewsItem | null> {
try {
const response = await HttpUtils.get<ApiResponse<NewsItem>>(
`${ApiConfig.NEWS_DETAIL}/${newsId}`
);
if (response.code === 200) {
return response.data;
}
return null;
} catch (error) {
console.error('获取新闻详情异常:', error);
return null;
}
}
/**
* 生成模拟新闻数据(开发测试用)
*/
static getMockNewsList(page: number, pageSize: number): NewsItem[] {
const mockList: NewsItem[] = [];
const startIndex = (page - 1) * pageSize;
const categories = ['科技', '财经', '体育', '娱乐', '社会'];
const sources = ['新华社', '人民日报', '央视新闻', '环球时报', '澎湃新闻'];
for (let i = 0; i < pageSize; i++) {
const index = startIndex + i;
mockList.push({
id: `news_${index}`,
title: `鸿蒙NEXT生态系统快速发展,开发者数量突破${index * 1000 + 500}万`,
summary: `随着鸿蒙NEXT的正式发布,越来越多的开发者加入鸿蒙生态,共同构建万物互联的智能世界...`,
imageUrl: `https://picsum.photos/400/200?random=${index}`,
source: sources[index % sources.length],
publishTime: new Date(Date.now() - index * 3600000).toISOString(),
category: categories[index % categories.length],
url: `https://example.com/news/${index}`,
readCount: Math.floor(Math.random() * 100000)
});
}
return mockList;
}
}
四、首页新闻列表实现
首页是App的门面,我们将实现一个功能完善的新闻列表,支持下拉刷新和上拉加载更多。
主页面布局
// Index.ets
import { HomeTab } from './HomeTab';
/**
* 主页面
* 使用Tabs实现底部导航
*/
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
@Builder
tabBuilder(title: string, index: number, icon: string, iconSelected: string) {
Column() {
Image(this.currentIndex === index ? iconSelected : icon)
.width(24)
.height(24)
.margin({ bottom: 4 })
Text(title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#007DFF' : '#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
build() {
Tabs({ barPosition: BarPosition.End }) {
// 首页Tab
TabContent() {
HomeTab()
}
.tabBar(this.tabBuilder('首页', 0, $r('app.media.tab_home'), $r('app.media.tab_home_selected')))
// 视频Tab(占位)
TabContent() {
Column() {
Text('视频').fontSize(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
.tabBar(this.tabBuilder('视频', 1, $r('app.media.tab_video'), $r('app.media.tab_video_selected')))
// 我的Tab(占位)
TabContent() {
Column() {
Text('我的').fontSize(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
.tabBar(this.tabBuilder('我的', 2, $r('app.media.tab_mine'), $r('app.media.tab_mine_selected')))
}
.barWidth('100%')
.barHeight(56)
.onChange((index: number) => {
this.currentIndex = index;
})
}
}
首页Tab内容
// HomeTab.ets
import { NewsList } from '../components/NewsList';
import { NewsCategory } from '../model/NewsModel';
/**
* 首页Tab内容
* 包含分类导航和新闻列表
*/
@Component
export struct HomeTab {
@State selectedCategory: string = NewsCategory.ALL;
// 分类列表
private categories: string[] = [
NewsCategory.ALL,
NewsCategory.TECH,
NewsCategory.FINANCE,
NewsCategory.SPORTS,
NewsCategory.ENTERTAINMENT
];
// 分类显示名称映射
private categoryNameMap: Record<string, string> = {
[NewsCategory.ALL]: '推荐',
[NewsCategory.TECH]: '科技',
[NewsCategory.FINANCE]: '财经',
[NewsCategory.SPORTS]: '体育',
[NewsCategory.ENTERTAINMENT]: '娱乐'
};
@Builder
categoryItem(category: string) {
Column() {
Text(this.categoryNameMap[category])
.fontSize(14)
.fontColor(this.selectedCategory === category ? '#007DFF' : '#333333')
.fontWeight(this.selectedCategory === category ? FontWeight.Bold : FontWeight.Normal)
}
.height(40)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.selectedCategory = category;
})
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('新闻资讯')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
}
.width('100%')
.height(48)
.padding({ left: 16 })
.justifyContent(FlexAlign.Start)
.backgroundColor('#FFFFFF')
// 分类导航
Scroll() {
Row() {
ForEach(this.categories, (category: string) => {
this.categoryItem(category)
})
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.height(40)
.backgroundColor('#FFFFFF')
.scrollBar(BarState.Off)
// 分割线
Divider()
.color('#F0F0F0')
.height(1)
// 新闻列表
NewsList({ category: this.selectedCategory })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
新闻卡片组件
// NewsCard.ets
import { NewsItem } from '../model/NewsModel';
import { DateUtils } from '../utils/DateUtils';
/**
* 新闻卡片组件
* 展示单条新闻信息
*/
@Component
export struct NewsCard {
news: NewsItem = {} as NewsItem;
onClick?: (news: NewsItem) => void;
build() {
Row() {
// 左侧文字内容
Column() {
// 新闻标题
Text(this.news.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ bottom: 8 })
// 底部信息栏
Row() {
// 新闻来源
Text(this.news.source)
.fontSize(12)
.fontColor('#999999')
Blank()
// 发布时间
Text(DateUtils.getRelativeTime(this.news.publishTime))
.fontSize(12)
.fontColor('#999999')
// 阅读量
if (this.news.readCount > 0) {
Text(`${this.formatReadCount(this.news.readCount)}阅读`)
.fontSize(12)
.fontColor('#999999')
.margin({ left: 8 })
}
}
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ right: 12 })
// 右侧封面图片
Image(this.news.imageUrl)
.width(120)
.height(80)
.borderRadius(4)
.objectFit(ImageFit.Cover)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.onClick(() => {
if (this.onClick) {
this.onClick(this.news);
}
})
}
/**
* 格式化阅读量
*/
private formatReadCount(count: number): string {
if (count >= 10000) {
return (count / 10000).toFixed(1) + 'w';
}
return count.toString();
}
}
新闻列表组件(下拉刷新 + 上拉加载)
// NewsList.ets
import { NewsCard } from './NewsCard';
import { NewsItem } from '../model/NewsModel';
import { NewsService } from '../services/NewsService';
import { router } from '@kit.ArkUI';
/**
* 新闻列表组件
* 支持下拉刷新和上拉加载更多
*/
@Component
export struct NewsList {
@State newsList: NewsItem[] = [];
@State isRefreshing: boolean = false;
@State isLoadingMore: boolean = false;
@State currentPage: number = 1;
@State hasMore: boolean = true;
@State isFirstLoad: boolean = true;
// 当前分类
category: string = 'all';
// 每页数量
private pageSize: number = 20;
aboutToAppear() {
this.loadNewsList();
}
/**
* 加载新闻列表
* @param isRefresh 是否是刷新操作
*/
async loadNewsList(isRefresh: boolean = false) {
if (isRefresh) {
this.currentPage = 1;
this.hasMore = true;
}
try {
const newsList = await NewsService.getNewsList(
this.currentPage,
this.pageSize,
this.category
);
if (isRefresh) {
this.newsList = newsList;
} else {
this.newsList = [...this.newsList, ...newsList];
}
// 判断是否还有更多数据
this.hasMore = newsList.length >= this.pageSize;
this.currentPage++;
} catch (error) {
console.error('加载新闻列表失败:', error);
} finally {
this.isRefreshing = false;
this.isLoadingMore = false;
this.isFirstLoad = false;
}
}
/**
* 下拉刷新回调
*/
onRefresh() {
this.isRefreshing = true;
this.loadNewsList(true);
}
/**
* 上拉加载更多回调
*/
onLoadMore() {
if (this.isLoadingMore || !this.hasMore) {
return;
}
this.isLoadingMore = true;
this.loadNewsList(false);
}
/**
* 跳转到详情页
*/
goToDetail(news: NewsItem) {
router.pushUrl({
url: 'pages/DetailPage',
params: {
newsId: news.id,
newsTitle: news.title,
newsUrl: news.url
}
});
}
build() {
Stack() {
// 新闻列表
List({ space: 1 }) {
// 下拉刷新容器
Refresh({ refreshing: $$this.isRefreshing }) {
List({ space: 1 }) {
// 新闻卡片列表
ForEach(this.newsList, (news: NewsItem) => {
ListItem() {
NewsCard({
news: news,
onClick: (clickedNews: NewsItem) => {
this.goToDetail(clickedNews);
}
})
}
})
// 加载更多提示
if (this.isLoadingMore) {
ListItem() {
Row() {
LoadingProgress()
.width(20)
.height(20)
.margin({ right: 8 })
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
}
// 没有更多数据提示
if (!this.hasMore && this.newsList.length > 0) {
ListItem() {
Text('—— 已经到底了 ——')
.fontSize(14)
.fontColor('#999999')
.width('100%')
.height(50)
.textAlign(TextAlign.Center)
}
}
}
.width('100%')
.height('100%')
.divider({ color: '#F0F0F0', height: 1 })
.onReachEnd(() => {
this.onLoadMore();
})
}
.onRefreshing(() => {
this.onRefresh();
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.edgeEffect(EdgeEffect.Spring)
// 空状态视图
if (this.newsList.length === 0 && !this.isFirstLoad) {
Column() {
Image($r('app.media.empty'))
.width(120)
.height(120)
.margin({ bottom: 16 })
Text('暂无新闻内容')
.fontSize(16)
.fontColor('#999999')
.margin({ bottom: 16 })
Button('刷新重试')
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('#007DFF')
.borderRadius(20)
.height(36)
.onClick(() => {
this.loadNewsList(true);
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// 首次加载状态
if (this.isFirstLoad) {
Column() {
LoadingProgress()
.width(40)
.height(40)
.margin({ bottom: 16 })
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
}
日期工具类
// DateUtils.ets
/**
* 日期工具类
*/
export class DateUtils {
/**
* 获取相对时间描述
* @param dateString 日期字符串
* @returns 相对时间描述
*/
static getRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) {
return '刚刚';
} else if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else if (days < 7) {
return `${days}天前`;
} else {
return `${date.getMonth() + 1}-${date.getDate()}`;
}
}
/**
* 格式化日期
* @param date 日期对象
* @param format 格式字符串
* @returns 格式化后的日期字符串
*/
static format(date: Date, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
}
五、新闻详情页实现
新闻详情页使用WebView加载网页内容,实现完整的新闻阅读体验。
详情页代码
// DetailPage.ets
import { webview } from '@kit.ArkWeb';
import { router } from '@kit.ArkUI';
/**
* 新闻详情页
* 使用WebView展示新闻内容
*/
@Entry
@Component
struct DetailPage {
@State newsTitle: string = '';
@State newsUrl: string = '';
@State isLoading: boolean = true;
@State loadProgress: number = 0;
// WebView控制器
private webviewController: webview.WebviewController = new webview.WebviewController();
aboutToAppear() {
// 获取路由参数
const params = router.getParams() as Record<string, string>;
if (params) {
this.newsTitle = params.newsTitle || '新闻详情';
this.newsUrl = params.newsUrl || 'https://www.example.com';
}
}
build() {
Column() {
// 顶部导航栏
Row() {
// 返回按钮
Image($r('app.media.back'))
.width(24)
.height(24)
.onClick(() => {
router.back();
})
// 标题
Text(this.newsTitle)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ left: 12 })
.layoutWeight(1)
// 分享按钮
Image($r('app.media.share'))
.width(24)
.height(24)
.onClick(() => {
// 分享功能(下篇实现)
console.info('分享功能开发中...');
})
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.border({ width: { bottom: 1 }, color: '#F0F0F0' })
// 加载进度条
if (this.isLoading) {
Progress({ value: this.loadProgress, total: 100, type: ProgressType.Linear })
.width('100%')
.height(2)
.color('#007DFF')
}
// WebView容器
Web({ src: this.newsUrl, controller: this.webviewController })
.width('100%')
.layoutWeight(1)
.javaScriptAccess(true)
.onPageBegin((event) => {
this.isLoading = true;
console.info('页面开始加载:', event.url);
})
.onPageEnd((event) => {
this.isLoading = false;
console.info('页面加载完成:', event.url);
})
.onProgressChange((event) => {
this.loadProgress = event.newProgress;
})
.onErrorReceive((event) => {
this.isLoading = false;
console.error('页面加载错误:', event.errorInfo);
})
// 底部操作栏
Row() {
// 评论按钮
Column() {
Image($r('app.media.comment'))
.width(24)
.height(24)
Text('评论')
.fontSize(10)
.fontColor('#666666')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
console.info('评论功能开发中...');
})
// 点赞按钮
Column() {
Image($r('app.media.like'))
.width(24)
.height(24)
Text('点赞')
.fontSize(10)
.fontColor('#666666')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
console.info('点赞功能开发中...');
})
// 收藏按钮
Column() {
Image($r('app.media.collect'))
.width(24)
.height(24)
Text('收藏')
.fontSize(10)
.fontColor('#666666')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
console.info('收藏功能开发中...');
})
// 字体大小按钮
Column() {
Image($r('app.media.font_size'))
.width(24)
.height(24)
Text('字号')
.fontSize(10)
.fontColor('#666666')
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
console.info('字号设置功能开发中...');
})
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.border({ width: { top: 1 }, color: '#F0F0F0' })
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
网络安全配置
为了让WebView能够正常加载网页,需要在module.json5中配置网络安全:
// module.json5
{
"module": {
"name": "entry",
"type": "entry",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets"
}
],
"networkStack": {
"cleartextTraffic": true,
"securityConfig": {
"domain": [
{
"name": "api.example.com",
"cleartextPermitted": true
}
]
}
}
}
}
六、页面路由配置
在鸿蒙中,页面路由需要在配置文件中声明。确保pages配置正确:
// src/main/resources/base/profile/main_pages.json
{
"src": [
"pages/Index",
"pages/DetailPage"
]
}
七、总结与预告
本篇总结
本篇我们完成了新闻资讯App的核心功能开发:
-
项目架构设计:采用分层架构,代码结构清晰
-
网络请求封装:基于axios封装统一的请求工具类
-
首页新闻列表:
-
使用List组件实现列表渲染
-
Refresh组件实现下拉刷新
-
onReachEnd事件实现上拉加载更多
-
新闻卡片组件复用
-
-
新闻详情页:
-
使用WebView加载新闻详情
-
实现加载进度条
-
底部操作栏布局
-
关键知识点回顾
|
知识点 |
说明 |
|---|---|
|
|
网络请求库,支持拦截器配置 |
|
|
下拉刷新容器组件 |
|
|
列表滑动到底部事件 |
|
|
WebView组件,用于加载网页 |
|
|
页面路由跳转 |
下篇预告
在下篇中,我们将继续完善App功能:
-
搜索功能实现
-
新闻收藏管理
-
个人中心页面
-
本地数据持久化
-
应用状态管理优化
敬请期待!
系列文章推荐
标签:鸿蒙实战 新闻App 完整项目 ArkUI 网络请求 下拉刷新 上拉加载 WebView 分层架构
💡 学习建议:动手实践是掌握开发技能的最佳方式。建议读者跟随教程一步步完成代码编写,遇到问题时查阅官方文档或在评论区交流讨论。
版权声明:本文为原创技术博客,欢迎转载分享,请注明出处。
更多推荐


所有评论(0)