鸿蒙跨端新闻阅读器:多设备同步新闻列表与阅读状态 原创

进修的泡芙
发布于 2025-6-18 21:20
浏览
0收藏

鸿蒙跨端新闻阅读器:多设备同步新闻列表与阅读状态

本文将基于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请求获取新闻数据,再通过分布式数据管理实现阅读状态的跨设备同步,为用户提供了无缝的跨设备阅读体验。

这种架构不仅适用于新闻应用,也可以扩展到博客阅读、文档查看等需要多设备同步阅读状态的场景。合理利用鸿蒙的分布式能力,可以显著提升用户体验和应用价值。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐