
鸿蒙跨端新闻阅读器:多设备同步新闻列表与阅读状态 原创
鸿蒙跨端新闻阅读器:多设备同步新闻列表与阅读状态
本文将基于HarmonyOS 5的HTTP网络请求和分布式能力,实现一个支持多设备同步的新闻阅读应用,能够在不同设备间同步新闻列表和阅读进度。
技术架构
数据获取层:通过HTTP请求获取新闻数据
数据处理层:解析和转换API返回的新闻数据
数据同步层:使用分布式数据管理同步阅读状态
UI展示层:响应式展示新闻列表和详情
完整代码实现
新闻数据模型定义
// model/NewsItem.ts
export class NewsItem {
id: string = ‘’; // 新闻ID
title: string = ‘’; // 新闻标题
summary: string = ‘’; // 新闻摘要
content: string = ‘’; // 新闻内容
source: string = ‘’; // 新闻来源
publishTime: number = 0; // 发布时间戳
imageUrl: string = ‘’; // 新闻图片URL
category: string = ‘’; // 新闻分类
isRead: boolean = false; // 是否已读
readingProgress: number = 0; // 阅读进度(0-100)
lastReadDevice: string = ‘’; // 最后阅读的设备ID
constructor(data?: any) {
if (data) {
this.id = data.id || ‘’;
this.title = data.title || ‘’;
this.summary = data.summary || ‘’;
this.content = data.content || ‘’;
this.source = data.source || ‘’;
this.publishTime = data.publishTime || Date.now();
this.imageUrl = data.imageUrl || ‘’;
this.category = data.category || ‘’;
}
// 格式化发布时间
get formattedTime(): string {
const date = new Date(this.publishTime);
return {date.getFullYear()}-{(date.getMonth() + 1).toString().padStart(2, ‘0’)}-${date.getDate().toString().padStart(2, ‘0’)};
}
新闻API服务实现
// service/NewsApiService.ts
import http from ‘@ohos.net.http’;
import { NewsItem } from ‘…/model/NewsItem’;
const NEWS_API = ‘https://newsapi.example.com/v2/top-headlines’;
const API_KEY = ‘YOUR_API_KEY’; // 替换为实际的API Key
export class NewsApiService {
private httpRequest = http.createHttp();
// 获取新闻列表
async fetchNewsList(category: string = ‘general’): Promise<NewsItem[]> {
return new Promise((resolve, reject) => {
this.httpRequest.request(
{NEWS_API}?country=cn&category={category}&apiKey=${API_KEY},
method: ‘GET’,
header: {
'Content-Type': 'application/json'
},
(err, data) => {
if (err) {
console.error('请求失败:', err);
reject(err);
return;
if (data.responseCode === 200) {
const result = JSON.parse(data.result);
const newsList = result.articles.map((article: any) =>
new NewsItem({
id: article.url.hashCode(),
title: article.title,
summary: article.description,
content: article.content,
source: article.source.name,
publishTime: new Date(article.publishedAt).getTime(),
imageUrl: article.urlToImage,
category: category
})
);
resolve(newsList);
else {
reject(new Error(请求失败,状态码: ${data.responseCode}));
}
);
});
// 获取新闻详情
async fetchNewsDetail(newsId: string): Promise<NewsItem> {
// 实际项目中这里应该调用获取单条新闻详情的API
// 这里简化为返回一个模拟数据
return new Promise(resolve => {
setTimeout(() => {
resolve(new NewsItem({
id: newsId,
title: ‘示例新闻标题’,
content: ‘这里是新闻的详细内容…’.repeat(50),
source: ‘示例来源’,
publishTime: Date.now() - 3600000
}));
}, 500);
});
}
分布式新闻同步服务
// service/NewsSyncService.ts
import distributedData from ‘@ohos.data.distributedData’;
import deviceInfo from ‘@ohos.deviceInfo’;
import { NewsItem } from ‘…/model/NewsItem’;
const STORE_ID = ‘news_sync_store’;
const NEWS_KEY_PREFIX = ‘news_’;
const READING_STATUS_KEY = ‘reading_status_’;
export class NewsSyncService {
private kvManager: distributedData.KVManager;
private kvStore: distributedData.SingleKVStore;
private localDeviceId: string = deviceInfo.deviceId;
// 初始化分布式数据存储
async initialize() {
const config = {
bundleName: ‘com.example.newsapp’,
userInfo: {
userId: ‘news_user’,
userType: distributedData.UserType.SAME_USER_ID
};
this.kvManager = distributedData.createKVManager(config);
const options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
};
this.kvStore = await this.kvManager.getKVStore(STORE_ID, options);
// 订阅数据变更
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
this.handleDataChange(data);
});
// 处理数据变更
private handleDataChange(data: distributedData.ChangeNotification) {
if (data.insertEntries.length > 0) {
data.insertEntries.forEach(entry => {
if (entry.key.startsWith(READING_STATUS_KEY)) {
const newsId = entry.key.substring(READING_STATUS_KEY.length);
const status = JSON.parse(entry.value.value);
// 更新AppStorage中的阅读状态
const newsMap: Map<string, NewsItem> = AppStorage.get('newsMap') || new Map();
const news = newsMap.get(newsId);
if (news) {
news.isRead = status.isRead;
news.readingProgress = status.progress;
news.lastReadDevice = status.deviceId;
newsMap.set(newsId, news);
AppStorage.setOrCreate('newsMap', newsMap);
}
});
}
// 同步新闻阅读状态
async syncReadingStatus(newsId: string, isRead: boolean, progress: number) {
const status = {
isRead,
progress,
deviceId: this.localDeviceId,
timestamp: Date.now()
};
await this.kvStore.put({READING_STATUS_KEY}{newsId}, JSON.stringify(status));
// 同步新闻列表
async syncNewsList(newsList: NewsItem[]) {
const newsMap = new Map<string, NewsItem>();
newsList.forEach(news => newsMap.set(news.id, news));
AppStorage.setOrCreate(‘newsMap’, newsMap);
// 获取当前设备ID
getLocalDeviceId(): string {
return this.localDeviceId;
}
新闻列表页面实现
// pages/NewsListPage.ets
import { NewsApiService } from ‘…/service/NewsApiService’;
import { NewsSyncService } from ‘…/service/NewsSyncService’;
import { NewsItem } from ‘…/model/NewsItem’;
@Entry
@Component
struct NewsListPage {
private newsApi = new NewsApiService();
private syncService = new NewsSyncService();
@State isLoading: boolean = true;
@State selectedCategory: string = ‘general’;
@StorageLink(‘newsMap’) newsMap: Map<string, NewsItem> = new Map();
@State categories: string[] = [‘general’, ‘technology’, ‘business’, ‘sports’, ‘entertainment’];
async aboutToAppear() {
await this.syncService.initialize();
await this.loadNews(this.selectedCategory);
this.isLoading = false;
build() {
Column() {
// 分类选择
Scroll(.horizontal) {
Row() {
ForEach(this.categories, (category: string) => {
Button(category)
.margin(5)
.stateEffect(true)
.backgroundColor(this.selectedCategory === category ? '#007DFF' : '#F5F5F5')
.fontColor(this.selectedCategory === category ? '#FFFFFF' : '#333333')
.onClick(() => {
this.changeCategory(category);
})
})
.padding(10)
.width(‘100%’)
// 加载状态
if (this.isLoading) {
LoadingProgress()
.width(50)
.height(50)
.margin({ top: 100 })
else {
// 新闻列表
List() {
ForEach(Array.from(this.newsMap.values()), (news: NewsItem) => {
ListItem() {
NewsListItem({
news: news,
onItemClick: () => {
this.openNewsDetail(news);
})
})
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
// 加载新闻
private async loadNews(category: string) {
this.isLoading = true;
try {
const newsList = await this.newsApi.fetchNewsList(category);
await this.syncService.syncNewsList(newsList);
catch (err) {
prompt.showToast({ message: '加载新闻失败', duration: 2000 });
finally {
this.isLoading = false;
}
// 切换分类
private async changeCategory(category: string) {
if (this.selectedCategory !== category) {
this.selectedCategory = category;
await this.loadNews(category);
}
// 打开新闻详情
private openNewsDetail(news: NewsItem) {
// 标记为已读并同步状态
news.isRead = true;
news.readingProgress = 100;
news.lastReadDevice = this.syncService.getLocalDeviceId();
this.newsMap.set(news.id, news);
this.syncService.syncReadingStatus(news.id, true, 100);
router.pushUrl({
url: 'pages/NewsDetailPage',
params: { newsId: news.id }
});
}
@Component
struct NewsListItem {
@Prop news: NewsItem;
@Prop onItemClick: () => void;
build() {
Row() {
// 新闻图片
if (this.news.imageUrl) {
Image(this.news.imageUrl)
.width(120)
.height(80)
.objectFit(ImageFit.Cover)
.margin({ right: 12 })
Column() {
// 新闻标题
Text(this.news.title)
.fontSize(18)
.fontColor(this.news.isRead ? '#888888' : '#000000')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ bottom: 4 })
// 新闻来源和时间
Row() {
Text(this.news.source)
.fontSize(12)
.fontColor('#888888')
Text(this.news.formattedTime)
.fontSize(12)
.fontColor('#888888')
.margin({ left: 10 })
// 阅读状态
if (this.news.isRead) {
Row() {
Circle()
.width(8)
.height(8)
.fill('#4CAF50')
.margin({ right: 4 })
Text('已读')
.fontSize(12)
.fontColor('#888888')
.margin({ top: 6 })
}
.layoutWeight(1)
.width(‘100%’)
.padding(12)
.borderRadius(8)
.backgroundColor('#FFFFFF')
.margin({ bottom: 8 })
.onClick(() => {
this.onItemClick();
})
}
新闻详情页面实现
// pages/NewsDetailPage.ets
import { NewsApiService } from ‘…/service/NewsApiService’;
import { NewsSyncService } from ‘…/service/NewsSyncService’;
@Entry
@Component
struct NewsDetailPage {
private newsApi = new NewsApiService();
private syncService = new NewsSyncService();
@State newsDetail: NewsItem = new NewsItem();
@State currentProgress: number = 0;
private scrollController: ScrollController = new ScrollController();
onPageShow(params: any) {
if (params?.newsId) {
this.loadNewsDetail(params.newsId);
// 监听滚动事件更新阅读进度
this.scrollController.addScrollListener((offset: number) => {
this.updateReadingProgress(offset);
});
build() {
Column() {
// 返回按钮
Row() {
Button('返回')
.onClick(() => {
router.back();
})
.width(‘100%’)
.padding(12)
// 新闻标题
Text(this.newsDetail.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
.width('90%')
// 新闻来源和时间
Row() {
Text(this.newsDetail.source)
.fontSize(14)
.fontColor('#888888')
Text(this.newsDetail.formattedTime)
.fontSize(14)
.fontColor('#888888')
.margin({ left: 10 })
.width(‘90%’)
.margin({ bottom: 20 })
// 新闻图片
if (this.newsDetail.imageUrl) {
Image(this.newsDetail.imageUrl)
.width('90%')
.height(200)
.objectFit(ImageFit.Cover)
.margin({ bottom: 20 })
// 新闻内容
Scroll(this.scrollController) {
Text(this.newsDetail.content)
.fontSize(16)
.lineHeight(24)
.width('90%')
.width(‘100%’)
.layoutWeight(1)
// 阅读进度
if (this.currentProgress > 0 && this.currentProgress < 100) {
Text(已阅读 ${this.currentProgress}%)
.fontSize(14)
.fontColor('#888888')
.margin({ bottom: 20 })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
// 加载新闻详情
private async loadNewsDetail(newsId: string) {
try {
const detail = await this.newsApi.fetchNewsDetail(newsId);
this.newsDetail = detail;
// 更新阅读进度
const newsMap: Map<string, NewsItem> = AppStorage.get('newsMap') || new Map();
const news = newsMap.get(newsId);
if (news) {
this.currentProgress = news.readingProgress;
} catch (err) {
prompt.showToast({ message: '加载新闻详情失败', duration: 2000 });
}
// 更新阅读进度
private updateReadingProgress(offset: number) {
// 简化的进度计算(实际应根据内容高度计算)
const newProgress = Math.min(Math.floor(offset / 10), 100);
if (newProgress > this.currentProgress) {
this.currentProgress = newProgress;
// 同步阅读进度
this.syncService.syncReadingStatus(
this.newsDetail.id,
this.currentProgress >= 80,
this.currentProgress
);
// 更新本地状态
const newsMap: Map<string, NewsItem> = AppStorage.get('newsMap') || new Map();
const news = newsMap.get(this.newsDetail.id);
if (news) {
news.readingProgress = this.currentProgress;
news.isRead = this.currentProgress >= 80;
news.lastReadDevice = this.syncService.getLocalDeviceId();
newsMap.set(this.newsDetail.id, news);
}
}
实现原理详解
数据获取与同步流程:
主设备从API获取新闻数据
数据解析后存储到AppStorage
阅读状态变更时同步到分布式数据库
其他设备接收变更后更新本地UI
阅读进度计算:
监听Scroll组件的滚动事件
根据滚动位置计算阅读进度
进度超过80%标记为已读
UI响应式更新:
使用@StorageLink绑定新闻数据
列表项根据阅读状态显示不同样式
详情页实时显示阅读进度
扩展功能建议
新闻收藏功能:
// 添加收藏功能
async toggleFavorite(newsId: string) {
const newsMap: Map<string, NewsItem> = AppStorage.get(‘newsMap’) || new Map();
const news = newsMap.get(newsId);
if (news) {
news.isFavorite = !news.isFavorite;
await this.syncService.syncFavoriteStatus(newsId, news.isFavorite);
}
离线缓存支持:
// 缓存新闻数据
async cacheNewsData(newsList: NewsItem[]) {
const cacheKey = news_cache_${this.selectedCategory};
await preferences.put(cacheKey, JSON.stringify(newsList));
新闻分享功能:
// 分享新闻到其他设备
async shareNewsToDevice(newsId: string, deviceId: string) {
const news = this.newsMap.get(newsId);
if (news) {
await this.syncService.sendToDevice(deviceId, ‘share_news’, news);
}
总结
本文展示了如何利用HarmonyOS的网络能力和分布式特性构建一个多设备同步的新闻阅读应用。通过HTTP请求获取新闻数据,再通过分布式数据管理实现阅读状态的跨设备同步,为用户提供了无缝的跨设备阅读体验。
这种架构不仅适用于新闻应用,也可以扩展到博客阅读、文档查看等需要多设备同步阅读状态的场景。合理利用鸿蒙的分布式能力,可以显著提升用户体验和应用价值。
