基于HarmonyOS Next的新闻客户端开发实战

一、项目概述与需求分析

在这个信息爆炸的时代,新闻类应用已经成为人们获取资讯的重要渠道。本教程将带领大家使用HarmonyOS Next和AppGallery Connect开发一个功能完善的新闻客户端,主要包含以下核心功能:

  1. 新闻内容展示:支持图文、视频等多种形式的内容呈现
  2. 个性化推荐:基于用户兴趣的智能推荐系统
  3. 离线阅读:支持新闻内容本地缓存
  4. 用户互动:评论、收藏等社交功能
// 应用入口文件
import { Ability, AbilityConstant, UIAbility, Want } from '@ohos.app.ability.UIAbility';
import { window } from '@ohos.window';

export default class NewsAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    console.info('NewsApplication onCreate');
    // 初始化全局上下文
    globalThis.newsContext = this.context;
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    console.info('NewsApplication onWindowStageCreate');
    // 加载首页
    windowStage.loadContent('pages/HomePage', (err) => {
      if (err.code) {
        console.error('加载首页失败:' + JSON.stringify(err));
        return;
      }
      console.info('首页加载成功');
    });
  }
}

二、技术架构设计

2.1 整体架构

我们采用分层架构设计,分为以下四层:

  1. 表现层:基于ArkUI的组件化界面
  2. 业务逻辑层:处理新闻相关的业务逻辑
  3. 数据层:使用AppGallery Connect的云数据库和存储服务
  4. 服务层:集成推送、分析等华为服务

2.2 技术选型

  1. UI框架:ArkUI声明式开发范式
  2. 状态管理:@State和@Prop装饰器
  3. 网络请求:内置HTTP模块
  4. 数据持久化:AppGallery Connect云数据库

三、新闻数据模型设计

新闻应用的核心是数据,我们需要设计合理的数据结构:

// 新闻数据模型
interface NewsItem {
  id: string; // 新闻ID
  title: string; // 新闻标题
  content: string; // 新闻内容
  coverUrl: string; // 封面图URL
  category: string; // 新闻分类
  publishTime: string; // 发布时间
  author: string; // 作者
  viewCount: number; // 阅读量
  likeCount: number; // 点赞数
  commentCount: number; // 评论数
  isVideo: boolean; // 是否为视频新闻
  videoUrl?: string; // 视频地址(可选)
}

// 评论数据模型
interface Comment {
  id: string;
  newsId: string;
  userId: string;
  content: string;
  createTime: string;
  likeCount: number;
}

四、新闻列表功能实现

新闻列表是用户最先接触的界面,需要精心设计:

// 新闻列表页面
@Entry
@Component
struct NewsListPage {
  @State newsList: NewsItem[] = [];
  @State isLoading: boolean = true;
  @State currentCategory: string = '推荐';

  // 分类标签数据
  private categories: string[] = ['推荐', '热点', '科技', '娱乐', '体育'];

  aboutToAppear() {
    this.loadNews(this.currentCategory);
  }

  // 加载新闻数据
  async loadNews(category: string) {
    this.isLoading = true;
    try {
      const db = agconnect.cloudDB();
      const query = db.createQuery();
      query.equalTo('category', category);
      query.orderByDesc('publishTime');
      const result = await db.executeQuery('News', query);
      this.newsList = result.getSnapshotObjects();
    } catch (error) {
      console.error('获取新闻列表失败:', error);
    } finally {
      this.isLoading = false;
    }
  }

  build() {
    Column() {
      // 分类标签栏
      Scroll(.horizontal) {
        Row() {
          ForEach(this.categories, (category: string) => {
            Text(category)
              .fontSize(18)
              .fontColor(this.currentCategory === category ? '#FF1948' : '#666')
              .margin({ right: 20 })
              .onClick(() => {
                this.currentCategory = category;
                this.loadNews(category);
              })
          })
        }
        .padding(15)
      }
      .scrollBar(BarState.Off)

      // 新闻列表
      List({ space: 15 }) {
        ForEach(this.newsList, (news: NewsItem) => {
          ListItem() {
            NewsListItem({ news: news })
          }
        }, (news: NewsItem) => news.id)
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

// 新闻列表项组件
@Component
struct NewsListItem {
  @Prop news: NewsItem;

  build() {
    Column() {
      // 新闻封面图
      if (this.news.isVideo) {
        Video({
          src: this.news.videoUrl,
          previewUri: this.news.coverUrl
        })
        .width('100%')
        .height(200)
      } else {
        Image(this.news.coverUrl)
          .width('100%')
          .height(200)
          .objectFit(ImageFit.Cover)
      }

      // 新闻标题和基本信息
      Column({ space: 8 }) {
        Text(this.news.title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Row() {
          Text(this.news.author)
            .fontSize(12)
            .fontColor('#999')
          
          Blank()
          
          Text(this.news.publishTime)
            .fontSize(12)
            .fontColor('#999')
        }
        .width('100%')
      }
      .padding(12)
    }
    .backgroundColor(Color.White)
    .borderRadius(8)
    .margin({ left: 12, right: 12 })
    .onClick(() => {
      router.pushUrl({
        url: 'pages/NewsDetailPage',
        params: { newsId: this.news.id }
      });
    })
  }
}

五、新闻详情页开发

新闻详情页需要展示完整内容和相关互动功能:

// 新闻详情页面
@Entry
@Component
struct NewsDetailPage {
  @State newsDetail: NewsItem | null = null;
  @State comments: Comment[] = [];
  @State isLiked: boolean = false;
  @State isCollected: boolean = false;

  private newsId: string = '';

  onPageShow() {
    this.newsId = router.getParams()?.newsId;
    this.loadNewsDetail();
    this.loadComments();
    this.checkUserStatus();
  }

  // 加载新闻详情
  async loadNewsDetail() {
    try {
      const db = agconnect.cloudDB();
      const query = db.createQuery();
      query.equalTo('id', this.newsId);
      const result = await db.executeQuery('News', query);
      const newsList = result.getSnapshotObjects();
      this.newsDetail = newsList.length > 0 ? newsList[0] : null;
    } catch (error) {
      console.error('获取新闻详情失败:', error);
    }
  }

  // 加载评论
  async loadComments() {
    try {
      const db = agconnect.cloudDB();
      const query = db.createQuery();
      query.equalTo('newsId', this.newsId);
      query.orderByDesc('createTime');
      const result = await db.executeQuery('Comments', query);
      this.comments = result.getSnapshotObjects();
    } catch (error) {
      console.error('获取评论失败:', error);
    }
  }

  // 检查用户点赞收藏状态
  async checkUserStatus() {
    const currentUser = agconnect.auth().getCurrentUser();
    if (!currentUser) return;

    try {
      const db = agconnect.cloudDB();
      
      // 检查点赞状态
      const likeQuery = db.createQuery();
      likeQuery.equalTo('newsId', this.newsId);
      likeQuery.equalTo('userId', currentUser.uid);
      const likeResult = await db.executeQuery('UserLikes', likeQuery);
      this.isLiked = likeResult.getSnapshotObjects().length > 0;

      // 检查收藏状态
      const collectQuery = db.createQuery();
      collectQuery.equalTo('newsId', this.newsId);
      collectQuery.equalTo('userId', currentUser.uid);
      const collectResult = await db.executeQuery('UserCollections', collectQuery);
      this.isCollected = collectResult.getSnapshotObjects().length > 0;
    } catch (error) {
      console.error('检查用户状态失败:', error);
    }
  }

  // 提交评论
  async submitComment(content: string) {
    const currentUser = agconnect.auth().getCurrentUser();
    if (!currentUser || !content.trim()) return;

    try {
      const db = agconnect.cloudDB();
      const newComment: Comment = {
        id: generateUUID(),
        newsId: this.newsId,
        userId: currentUser.uid,
        content: content,
        createTime: new Date().toISOString(),
        likeCount: 0
      };
      
      await db.insert('Comments', newComment);
      this.comments = [newComment, ...this.comments];
      
      // 更新新闻评论数
      if (this.newsDetail) {
        const updatedNews = { ...this.newsDetail, commentCount: this.newsDetail.commentCount + 1 };
        await db.update('News', updatedNews);
        this.newsDetail = updatedNews;
      }
    } catch (error) {
      console.error('提交评论失败:', error);
    }
  }

  build() {
    Column() {
      if (!this.newsDetail) {
        LoadingProgress()
          .height(60)
          .width(60)
      } else {
        // 新闻内容区域
        Scroll() {
          Column({ space: 20 }) {
            // 新闻标题
            Text(this.newsDetail.title)
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .margin({ top: 15, bottom: 10 })

            // 作者和发布时间
            Row() {
              Text(this.newsDetail.author)
                .fontSize(14)
                .fontColor('#666')
              
              Blank()
              
              Text(formatDate(this.newsDetail.publishTime))
                .fontSize(14)
                .fontColor('#666')
            }
            .width('100%')

            // 视频或图片内容
            if (this.newsDetail.isVideo) {
              Video({
                src: this.newsDetail.videoUrl,
                controller: new VideoController()
              })
              .width('100%')
              .height(220)
            } else if (this.newsDetail.coverUrl) {
              Image(this.newsDetail.coverUrl)
                .width('100%')
                .height(220)
                .objectFit(ImageFit.Cover)
            }

            // 新闻正文
            Text(this.newsDetail.content)
              .fontSize(16)
              .lineHeight(26)

            // 互动功能区
            Row({ space: 20 }) {
              // 点赞按钮
              Column() {
                Image(this.isLiked ? '/common/liked.png' : '/common/like.png')
                  .width(24)
                  .height(24)
                  .onClick(async () => {
                    await this.toggleLike();
                  })
                Text(this.newsDetail.likeCount.toString())
                  .fontSize(12)
              }

              // 收藏按钮
              Column() {
                Image(this.isCollected ? '/common/collected.png' : '/common/collect.png')
                  .width(24)
                  .height(24)
                  .onClick(async () => {
                    await this.toggleCollect();
                  })
              }

              // 评论按钮
              Column() {
                Image('/common/comment.png')
                  .width(24)
                  .height(24)
                Text(this.newsDetail.commentCount.toString())
                  .fontSize(12)
              }
            }
            .margin({ top: 20 })
            .width('100%')
            .justifyContent(FlexAlign.Center)

            // 评论区域
            Text('评论 (' + this.comments.length + ')')
              .fontSize(18)
              .fontWeight(FontWeight.Medium)
              .margin({ top: 30, bottom: 10 })

            // 评论列表
            ForEach(this.comments, (comment: Comment) => {
              CommentItem({ comment: comment })
            })
          }
          .padding(15)
        }

        // 底部评论输入框
        Row({ space: 10 }) {
          TextInput({ placeholder: '写下你的评论...' })
            .layoutWeight(1)
            .height(40)
            .borderRadius(20)
            .backgroundColor('#EEE')
            .padding({ left: 15, right: 15 })
          
          Button('发送')
            .width(60)
            .height(40)
            .fontSize(14)
            .onClick(() => {
              // 实现发送评论逻辑
            })
        }
        .padding(10)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#EEE' })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F9F9F9')
  }
}

六、个性化推荐功能实现

基于用户行为数据实现个性化推荐:

// 推荐服务类
export class RecommendationService {
  private readonly MAX_RECOMMENDATIONS = 20;

  // 获取个性化推荐新闻
  async getPersonalizedNews(userId: string): Promise<NewsItem[]> {
    try {
      // 1. 获取用户兴趣标签
      const userTags = await this.getUserTags(userId);
      
      // 2. 获取基于标签的推荐
      const tagBased = await this.getTagBasedRecommendations(userTags);
      
      // 3. 获取热门新闻(作为兜底)
      const popularNews = await this.getPopularNews();
      
      // 4. 合并并去重
      const allRecommendations = [...tagBased, ...popularNews];
      const uniqueRecommendations = this.removeDuplicates(allRecommendations);
      
      // 5. 随机排序并截取
      return this.shuffleArray(uniqueRecommendations)
        .slice(0, this.MAX_RECOMMENDATIONS);
    } catch (error) {
      console.error('获取推荐新闻失败:', error);
      return [];
    }
  }

  // 获取用户兴趣标签
  private async getUserTags(userId: string): Promise<string[]> {
    const db = agconnect.cloudDB();
    const query = db.createQuery();
    query.equalTo('userId', userId);
    const result = await db.executeQuery('UserTags', query);
    const tags = result.getSnapshotObjects();
    return tags.map(tag => tag.tagName);
  }

  // 基于标签获取推荐
  private async getTagBasedRecommendations(tags: string[]): Promise<NewsItem[]> {
    if (tags.length === 0) return [];
    
    const db = agconnect.cloudDB();
    const query = db.createQuery();
    query.in('tags', tags);
    query.orderByDesc('publishTime');
    query.limit(10);
    const result = await db.executeQuery('News', query);
    return result.getSnapshotObjects();
  }

  // 获取热门新闻
  private async getPopularNews(): Promise<NewsItem[]> {
    const db = agconnect.cloudDB();
    const query = db.createQuery();
    query.orderByDesc('viewCount');
    query.limit(10);
    const result = await db.executeQuery('News', query);
    return result.getSnapshotObjects();
  }

  // 数组去重
  private removeDuplicates(newsList: NewsItem[]): NewsItem[] {
    const seen = new Set();
    return newsList.filter(news => {
      const duplicate = seen.has(news.id);
      seen.add(news.id);
      return !duplicate;
    });
  }

  // 数组随机排序
  private shuffleArray(array: any[]): any[] {
    return array.sort(() => Math.random() - 0.5);
  }
}

七、离线阅读功能实现

// 离线缓存服务
export class OfflineService {
  private readonly CACHE_EXPIRE_DAYS = 7;

  // 缓存单条新闻
  async cacheNews(news: NewsItem): Promise<void> {
    try {
      // 1. 保存到本地数据库
      const localDB = relationalStore.getRdbStore(globalThis.newsContext, {
        name: 'NewsCache.db',
        securityLevel: relationalStore.SecurityLevel.S1
      });
      
      // 2. 创建表(如果不存在)
      await localDB.executeSql(
        'CREATE TABLE IF NOT EXISTS news_cache ' +
        '(id TEXT PRIMARY KEY, data TEXT, timestamp INTEGER)'
      );
      
      // 3. 插入或更新缓存
      const valueBucket = {
        'id': news.id,
        'data': JSON.stringify(news),
        'timestamp': new Date().getTime()
      };
      await localDB.insert('news_cache', valueBucket);
      
      // 4. 清理过期缓存
      await this.cleanExpiredCache();
    } catch (error) {
      console.error('缓存新闻失败:', error);
    }
  }

  // 获取缓存的新闻
  async getCachedNews(newsId: string): Promise<NewsItem | null> {
    try {
      const localDB = relationalStore.getRdbStore(globalThis.newsContext, {
        name: 'NewsCache.db',
        securityLevel: relationalStore.SecurityLevel.S1
      });
      
      const predicates = new relationalStore.RdbPredicates('news_cache');
      predicates.equalTo('id', newsId);
      
      const result = await localDB.query(predicates, ['data']);
      if (result.rowCount > 0) {
        return JSON.parse(result.get(0)?.data);
      }
      return null;
    } catch (error) {
      console.error('获取缓存新闻失败:', error);
      return null;
    }
  }

  // 清理过期缓存
  private async cleanExpiredCache(): Promise<void> {
    try {
      const expireTime = new Date().getTime() - (this.CACHE_EXPIRE_DAYS * 24 * 60 * 60 * 1000);
      
      const localDB = relationalStore.getRdbStore(globalThis.newsContext, {
        name: 'NewsCache.db',
        securityLevel: relationalStore.SecurityLevel.S1
      });
      
      const predicates = new relationalStore.RdbPredicates('news_cache');
      predicates.lessThanOrEqualTo('timestamp', expireTime);
      
      await localDB.delete(predicates);
    } catch (error) {
      console.error('清理缓存失败:', error);
    }
  }
}

八、应用优化与发布

8.1 性能优化建议

  1. 图片懒加载:使用LazyForEach优化长列表性能
  2. 数据预加载:提前加载用户可能浏览的内容
  3. 内存管理:及时释放不用的资源

8.2 发布准备

  1. 在AppGallery Connect中配置应用信息
  2. 准备多尺寸的应用图标和截图
  3. 编写清晰的应用描述和更新日志
  4. 测试各种网络环境下的表现
// 应用发布检查
async function checkBeforePublish() {
  // 1. 检查必要权限
  const permissions: Array<string> = [
    'ohos.permission.INTERNET',
    'ohos.permission.READ_MEDIA',
    'ohos.permission.WRITE_MEDIA'
  ];
  
  for (const permission of permissions) {
    const result = await abilityAccessCtrl.createAtManager().verifyAccessToken(
      globalThis.newsContext.tokenId,
      permission
    );
    if (result !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      console.error(`权限 ${permission} 未授予`);
    }
  }
  
  // 2. 检查云数据库连接
  try {
    const db = agconnect.cloudDB();
    const query = db.createQuery();
    query.limit(1);
    await db.executeQuery('News', query);
  } catch (error) {
    console.error('云数据库连接检查失败:', error);
  }
  
  // 3. 检查关键功能
  const testNewsId = 'test_news_001';
  const offlineService = new OfflineService();
  await offlineService.cacheNews({
    id: testNewsId,
    title: '测试新闻',
    content: '这是一个测试',
    // 其他必要字段...
  } as NewsItem);
  
  const cachedNews = await offlineService.getCachedNews(testNewsId);
  if (!cachedNews) {
    console.error('离线缓存功能异常');
  }
}

通过本教程,我们完成了一个基于HarmonyOS Next的新闻客户端核心功能开发。这个应用不仅具备良好的用户体验,还充分利用了HarmonyOS的分布式能力和AppGallery Connect的后端服务。希望这个案例能帮助你快速掌握HarmonyOS应用开发的关键技术。


林钟雪
4 声望0 粉丝