Practical Guide to Developing a News App Based on HarmonyOS Next
1. Project Overview and Environment Setup
With the release of HarmonyOS Next, developers can leverage its enhanced distributed capabilities and improved toolchain to build cross-device application experiences. This tutorial will guide you through developing a news app, covering core modules such as news listing, detailed viewing, and bookmarking functionality.
First, ensure your development environment is ready:
- Install the latest version of DevEco Studio (recommended 4.1 or above).
- When creating a project, select "Application" → "Empty Ability."
- Configure AppGallery Connect services (details will be covered later).
The project structure primarily consists of the following key components:
-
pages
: Contains page code. -
resources
: Stores application resource files. -
entryability
: Application entry point. -
model
: Data model layer.
2. AppGallery Connect Service Integration
AppGallery Connect provides robust backend services for HarmonyOS applications. First, complete the following configurations:
- Create a project in the AppGallery Connect console.
- Enable Authentication Service (Auth Service) and Cloud Database (Cloud DB).
- Download the configuration file
agconnect-services.json
and place it in the project'sentry
directory.
Add dependencies in build.gradle
:
// entry/build.gradle
dependencies {
implementation 'io.agconnect.agconnect-core-harmony:agconnect-core:1.6.0.300'
implementation 'io.agconnect.agconnect-auth-harmony:agconnect-auth:1.6.0.300'
implementation 'io.agconnect.agconnect-clouddb-harmony:agconnect-clouddb:1.6.0.300'
}
Initialization code:
// entryability/EntryAbility.ts
import agconnect from '@hw-agconnect/api-ohos';
import '@hw-agconnect/core-ohos';
import '@hw-agconnect/auth-ohos';
import '@hw-agconnect/clouddb-ohos';
onCreate() {
agconnect.instance().init(this.context);
// Initialize Cloud Database
this.initCloudDB();
}
async initCloudDB() {
try {
const cloudDBZoneConfig = new cloud.CloudDBZoneConfig('NewsDB');
this.cloudDBZone = await cloud.CloudDBZone.open(cloudDBZoneConfig);
} catch (err) {
console.error('CloudDB init failed: ' + JSON.stringify(err));
}
}
3. News Data Model Design
Define the news data object type for local and cloud storage. Create News.ts
in the model
directory:
// model/News.ts
import { cloud } from '@hw-agconnect/clouddb-ohos';
export class News {
// Use decorators to define Cloud DB fields
@cloud.Field()
id: string = '';
@cloud.Field()
title: string = '';
@cloud.Field()
content: string = '';
@cloud.Field()
author: string = '';
@cloud.Field()
publishTime: number = 0;
@cloud.Field()
category: string = '';
@cloud.Field()
imageUrl: string = '';
@cloud.Field()
isFavorite: boolean = false;
constructor() {
// Set object type name (must match the Cloud DB object type)
cloud.ObjectType.register(this, 'News');
}
}
After creating the corresponding object type in the Cloud DB console, you can perform CRUD operations on the data.
4. News Listing Page Development
The news listing is the app's core page. We'll use the List
component to display news data with pull-to-refresh and infinite scroll functionality.
// pages/NewsListPage.ets
import { News } from '../model/News';
import { cloud } from '@hw-agconnect/clouddb-ohos';
@Entry
@Component
struct NewsListPage {
@State newsList: Array<News> = [];
@State isLoading: boolean = false;
@State isRefreshing: boolean = false;
private pageSize: number = 10;
private pageIndex: number = 0;
build() {
Column() {
// Header
Row() {
Text('Top News')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Blank()
Image($r('app.media.ic_search'))
.width(30)
.height(30)
.margin({ right: 15 })
}
.width('100%')
.padding(15)
.backgroundColor('#FF4D4F')
// News list
List({ space: 10 }) {
ForEach(this.newsList, (item: News) => {
ListItem() {
NewsItem({ news: item })
}
}, (item: News) => item.id)
}
.width('100%')
.layoutWeight(1)
.onScrollIndex((start: number) => {
// Load more when scrolling to the bottom
if (start >= this.newsList.length - 3 && !this.isLoading) {
this.loadMoreNews();
}
})
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
.onAppear(() => {
this.refreshNews();
})
}
// Pull-to-refresh
async refreshNews() {
this.isRefreshing = true;
this.pageIndex = 0;
try {
const query = cloud.CloudDBZoneQuery.where(News).orderByDesc('publishTime').limit(this.pageSize);
const result = await this.cloudDBZone.executeQuery(query, News);
this.newsList = result;
} catch (err) {
console.error('Refresh news failed: ' + JSON.stringify(err));
}
this.isRefreshing = false;
}
// Load more
async loadMoreNews() {
this.isLoading = true;
this.pageIndex++;
try {
const query = cloud.CloudDBZoneQuery.where(News)
.orderByDesc('publishTime')
.limit(this.pageSize)
.offset(this.pageIndex * this.pageSize);
const result = await this.cloudDBZone.executeQuery(query, News);
this.newsList = this.newsList.concat(result);
} catch (err) {
console.error('Load more news failed: ' + JSON.stringify(err));
}
this.isLoading = false;
}
}
// News item component
@Component
struct NewsItem {
@Prop news: News;
build() {
Column() {
// News image
Image(this.news.imageUrl)
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
.borderRadius(8)
// Title and summary
Column() {
Text(this.news.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text(this.news.content.substring(0, 50) + '...')
.fontSize(14)
.fontColor('#666')
}
.padding(10)
// Footer info
Row() {
Text(this.news.author)
.fontSize(12)
.fontColor('#999')
Blank()
Text(this.formatTime(this.news.publishTime))
.fontSize(12)
.fontColor('#999')
Image(this.news.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
.width(20)
.height(20)
.margin({ left: 10 })
.onClick(() => {
this.toggleFavorite();
})
}
.width('100%')
.padding({ left: 10, right: 10, bottom: 10 })
}
.width('95%')
.margin({ top: 5, bottom: 5 })
.backgroundColor('#FFF')
.borderRadius(8)
.shadow({ radius: 4, color: '#10000000', offsetX: 0, offsetY: 2 })
.onClick(() => {
router.pushUrl({ url: 'pages/NewsDetailPage', params: { newsId: this.news.id } });
})
}
// Format timestamp
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()}`;
}
// Toggle bookmark
private async toggleFavorite() {
try {
this.news.isFavorite = !this.news.isFavorite;
await this.cloudDBZone.executeUpsert([this.news]);
} catch (err) {
console.error('Toggle favorite failed: ' + JSON.stringify(err));
}
}
}
5. News Detail Page Implementation
The news detail page displays the full content and supports bookmarking.
// pages/NewsDetailPage.ets
import { News } from '../model/News';
@Entry
@Component
struct NewsDetailPage {
@State news: News = new News();
private newsId: string = '';
onPageShow() {
this.newsId = router.getParams()?.newsId;
this.loadNewsDetail();
}
build() {
Column() {
// Back button and title
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.onClick(() => {
router.back();
})
Text('News Detail')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ left: 15 })
Blank()
Image(this.news.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
.width(24)
.height(24)
.onClick(() => {
this.toggleFavorite();
})
}
.width('100%')
.padding(15)
.backgroundColor('#FF4D4F')
// Content area
Scroll() {
Column() {
// Title and author info
Text(this.news.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Row() {
Text(`Author: ${this.news.author}`)
.fontSize(14)
.fontColor('#666')
Text(`Published: ${this.formatTime(this.news.publishTime)}`)
.fontSize(14)
.fontColor('#666')
.margin({ left: 15 })
}
.margin({ bottom: 15 })
// News image
Image(this.news.imageUrl)
.width('100%')
.height(250)
.objectFit(ImageFit.Cover)
.margin({ bottom: 20 })
// News content
Text(this.news.content)
.fontSize(16)
.lineHeight(26)
}
.padding(20)
}
.scrollBar(BarState.Off)
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
// Load news details
private async loadNewsDetail() {
try {
const query = cloud.CloudDBZoneQuery.where(News).equalTo('id', this.newsId);
const result = await this.cloudDBZone.executeQuery(query, News);
if (result && result.length > 0) {
this.news = result[0];
}
} catch (err) {
console.error('Load news detail failed: ' + JSON.stringify(err));
}
}
// Toggle bookmark
private async toggleFavorite() {
try {
this.news.isFavorite = !this.news.isFavorite;
await this.cloudDBZone.executeUpsert([this.news]);
} catch (err) {
console.error('Toggle favorite failed: ' + JSON.stringify(err));
}
}
// Format timestamp
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
}
}
6. User Authentication and Bookmarking
To enable bookmarking, implement user authentication using AppGallery Connect Auth Service.
// utils/AuthUtil.ts
import { agconnect } from '@hw-agconnect/api-ohos';
import '@hw-agconnect/auth-ohos';
export class AuthUtil {
// Anonymous login
static async anonymousLogin(): Promise<boolean> {
try {
const user = await agconnect.auth().signInAnonymously();
return user != null;
} catch (err) {
console.error('Anonymous login failed: ' + JSON.stringify(err));
return false;
}
}
// Get current user ID
static getCurrentUserId(): string | null {
const user = agconnect.auth().currentUser;
return user ? user.uid : null;
}
// Logout
static async logout(): Promise<void> {
try {
await agconnect.auth().signOut();
} catch (err) {
console.error('Logout failed: ' + JSON.stringify(err));
}
}
}
Modify the News
model to include user association:
// model/News.ts
export class News {
// ...existing fields
@cloud.Field()
userId: string = ''; // Associated user ID
// ...other code
}
Update bookmarking to check login status:
// In NewsItem component, modify toggleFavorite
private async toggleFavorite() {
if (!AuthUtil.getCurrentUserId()) {
const isLogin = await AuthUtil.anonymousLogin();
if (!isLogin) {
prompt.showToast({ message: 'Please log in first' });
return;
}
}
try {
this.news.isFavorite = !this.news.isFavorite;
this.news.userId = AuthUtil.getCurrentUserId() || '';
await this.cloudDBZone.executeUpsert([this.news]);
} catch (err) {
console.error('Toggle favorite failed: ' + JSON.stringify(err));
}
}
7. App Packaging and Release
After development, package and publish the app to AppGallery:
- In DevEco Studio, select Build → Generate Key and CSR to create a signing certificate.
- Configure signing information in
build-profile.json5
. - Select Build → Build HAP(s)/APP(s) → Build APP to generate the release package.
- Log in to the AppGallery Connect console, navigate to "My Apps," and create a new app.
- Upload the generated APP file, fill in app details, and submit for review.
8. Advanced Feature Suggestions
After completing the basics, consider adding:
- Category Filtering: Add category tabs for filtering news.
- Comments: Implement comment functionality using Cloud DB.
- Offline Reading: Use local storage for offline access.
- Push Notifications: Integrate AppGallery Connect Push Kit for breaking news alerts.
- Multi-Device Sync: Leverage HarmonyOS distributed capabilities for seamless device switching.
This tutorial provides a complete guide to developing a HarmonyOS Next-based news app, from setup to implementation. Use it as a foundation to build more sophisticated applications.
Top comments (0)