DEV Community

linzhongxue
linzhongxue

Posted on

Practical Development of News Applications Based on HarmonyOS Next

Practical Development of News Applications Based on HarmonyOS Next: Building an Intelligent Information Platform from Scratch

1. Project Overview and Environment Preparation

In this era of information explosion, developing a news application based on HarmonyOS Next holds significant practical importance. We will leverage various services provided by AppGallery Connect to create a comprehensive application with core features such as news browsing, personalized recommendations, and bookmark sharing.

Development Environment Requirements:

  • DevEco Studio 3.1 or later
  • HarmonyOS SDK API 9+
  • Registered Huawei Developer Account

Project Initialization Steps:

  1. Create a new project in DevEco Studio using the "Application" template
  2. Create a corresponding project in the AppGallery Connect console
  3. Configure the application's package name and signing certificate
// Basic configuration example in app.json5  
{
  "app": {
    "bundleName": "com.example.newshub",
    "vendor": "example",
    "versionCode": 1,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}
Enter fullscreen mode Exit fullscreen mode

2. News Data Acquisition and Display

2.1 Network Requests and Data Parsing

The core of a news application lies in obtaining real-time news data. We will use ArkTS's networking capabilities in conjunction with AppGallery Connect's cloud functions.

// Network request utility class  
import http from '@ohos.net.http';

class NewsService {
  private httpRequest = http.createHttp();

  // Fetch news list  
  async fetchNewsList(category: string, page: number): Promise<NewsItem[]> {
    let url = `https://your-api-endpoint.com/news?category=${category}&page=${page}`;

    try {
      let response = await this.httpRequest.request(
        url,
        { method: 'GET' }
      );

      if (response.responseCode === 200) {
        let result = JSON.parse(response.result.toString());
        return result.data as NewsItem[];
      } else {
        console.error(`Request failed, status code: ${response.responseCode}`);
        return [];
      }
    } catch (error) {
      console.error(`Request error: ${error.message}`);
      return [];
    }
  }
}

// News data model  
interface NewsItem {
  id: string;
  title: string;
  summary: string;
  content: string;
  imageUrl: string;
  publishTime: string;
  source: string;
  category: string;
}
Enter fullscreen mode Exit fullscreen mode

2.2 Implementing the News List Interface

Use ArkUI's List component to display the news list and implement pull-to-refresh and infinite scrolling functionalities.

@Entry
@Component
struct NewsListPage {
  @State newsList: NewsItem[] = [];
  @State currentCategory: string = 'technology';
  @State currentPage: number = 1;
  @State isLoading: boolean = false;
  @State isRefreshing: boolean = false;

  private newsService = new NewsService();

  // Lifecycle function - load data when page appears  
  onPageShow() {
    this.loadData();
  }

  // Load data  
  async loadData() {
    this.isLoading = true;
    let newData = await this.newsService.fetchNewsList(this.currentCategory, this.currentPage);
    this.newsList = [...this.newsList, ...newData];
    this.isLoading = false;
  }

  // Pull-to-refresh  
  async onRefresh() {
    this.isRefreshing = true;
    this.currentPage = 1;
    this.newsList = [];
    await this.loadData();
    this.isRefreshing = false;
  }

  // Infinite scroll  
  async loadMore() {
    if (this.isLoading) return;
    this.currentPage++;
    await this.loadData();
  }

  build() {
    Column() {
      // Category selector  
      CategoryTabs({
        categories: ['technology', 'business', 'sports', 'entertainment'],
        currentCategory: this.currentCategory,
        onCategoryChange: (category) => {
          this.currentCategory = category;
          this.onRefresh();
        }
      })

      // News list  
      List({ space: 10 }) {
        ForEach(this.newsList, (item: NewsItem) => {
          ListItem() {
            NewsCard({ newsItem: item })
              .onClick(() => {
                router.pushUrl({ url: 'pages/DetailPage', params: { newsId: item.id } });
              })
          }
        }, (item: NewsItem) => item.id)

        // Loading indicator  
        if (this.isLoading && !this.isRefreshing) {
          ListItem() {
            LoadingIndicator()
              .height(60)
              .width('100%')
          }
        }
      }
      .width('100%')
      .height('100%')
      .onScrollIndex((startIndex: number) => {
        // Automatically load more when scrolling to bottom  
        if (startIndex >= this.newsList.length - 3) {
          this.loadMore();
        }
      })
    }
    .width('100%')
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

3. User Authentication and Personalized Features

3.1 Integrating AppGallery Connect Authentication Service

import { agconnect } from '@hw-agconnect/api-ohos';
import '@hw-agconnect/auth-ohos';

class AuthService {
  // User login  
  async login(email: string, password: string): Promise<boolean> {
    try {
      await agconnect.auth().signIn(email, password);
      return true;
    } catch (error) {
      console.error('Login failed:', error);
      return false;
    }
  }

  // User registration  
  async register(email: string, password: string): Promise<boolean> {
    try {
      await agconnect.auth().createUser(email, password);
      return true;
    } catch (error) {
      console.error('Registration failed:', error);
      return false;
    }
  }

  // Get current user  
  getCurrentUser(): agconnect.auth.AuthUser | null {
    return agconnect.auth().currentUser;
  }

  // User logout  
  async logout(): Promise<void> {
    await agconnect.auth().signOut();
  }
}
Enter fullscreen mode Exit fullscreen mode

3.2 Implementing News Bookmarking

// Bookmark service  
class FavoriteService {
  private cloudDB: agconnect.cloudDB.CloudDBZone;

  constructor() {
    const config = {
      name: 'FavoriteZone',
      persistenceEnabled: true
    };
    this.cloudDB = agconnect.cloudDB.CloudDBZoneWrapper.openCloudDBZone(config);
  }

  // Add bookmark  
  async addFavorite(newsId: string, userId: string): Promise<boolean> {
    try {
      const favorite = {
        id: `${userId}_${newsId}`,
        newsId,
        userId,
        createTime: new Date().getTime()
      };
      await this.cloudDB.executeUpsert('Favorite', [favorite]);
      return true;
    } catch (error) {
      console.error('Bookmark failed:', error);
      return false;
    }
  }

  // Remove bookmark  
  async removeFavorite(newsId: string, userId: string): Promise<boolean> {
    try {
      const query = agconnect.cloudDB.CloudDBZoneQuery.where('Favorite')
        .equalTo('id', `${userId}_${newsId}`);
      await this.cloudDB.executeDelete(query);
      return true;
    } catch (error) {
      console.error('Unbookmark failed:', error);
      return false;
    }
  }

  // Check if bookmarked  
  async isFavorite(newsId: string, userId: string): Promise<boolean> {
    const query = agconnect.cloudDB.CloudDBZoneQuery.where('Favorite')
      .equalTo('id', `${userId}_${newsId}`);
    const result = await this.cloudDB.executeQuery(query);
    return result.length > 0;
  }

  // Get user bookmarks  
  async getUserFavorites(userId: string): Promise<Favorite[]> {
    const query = agconnect.cloudDB.CloudDBZoneQuery.where('Favorite')
      .equalTo('userId', userId)
      .orderByDesc('createTime');
    return await this.cloudDB.executeQuery(query);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. News Details and Interaction Features

4.1 Implementing the News Detail Page

@Entry
@Component
struct NewsDetailPage {
  @State newsDetail: NewsDetail | null = null;
  @State isFavorite: boolean = false;
  @State comments: Comment[] = [];
  @State newComment: string = '';

  private newsId: string = '';
  private newsService = new NewsService();
  private favoriteService = new FavoriteService();
  private commentService = new CommentService();
  private currentUser = agconnect.auth().currentUser;

  onPageShow() {
    const params = router.getParams();
    this.newsId = params?.newsId || '';
    this.loadNewsDetail();
    this.loadComments();
    this.checkFavoriteStatus();
  }

  async loadNewsDetail() {
    this.newsDetail = await this.newsService.fetchNewsDetail(this.newsId);
  }

  async checkFavoriteStatus() {
    if (this.currentUser) {
      this.isFavorite = await this.favoriteService.isFavorite(
        this.newsId, 
        this.currentUser.uid
      );
    }
  }

  async toggleFavorite() {
    if (!this.currentUser) {
      prompt.showToast({ message: 'Please login first' });
      return;
    }

    if (this.isFavorite) {
      await this.favoriteService.removeFavorite(this.newsId, this.currentUser.uid);
    } else {
      await this.favoriteService.addFavorite(this.newsId, this.currentUser.uid);
    }
    this.isFavorite = !this.isFavorite;
  }

  async loadComments() {
    this.comments = await this.commentService.getCommentsForNews(this.newsId);
  }

  async submitComment() {
    if (!this.currentUser || !this.newComment.trim()) return;

    await this.commentService.addComment({
      newsId: this.newsId,
      userId: this.currentUser.uid,
      userName: this.currentUser.displayName || 'Anonymous',
      content: this.newComment,
      createTime: new Date().getTime()
    });

    this.newComment = '';
    await this.loadComments();
  }

  build() {
    Column() {
      if (this.newsDetail) {
        Scroll() {
          Column({ space: 15 }) {
            // News title section  
            Text(this.newsDetail.title)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 10 })

            // News source and time  
            Row() {
              Text(this.newsDetail.source)
                .fontSize(14)
                .fontColor('#666')

              Text(this.newsDetail.publishTime)
                .fontSize(14)
                .fontColor('#666')
                .margin({ left: 15 })
            }
            .justifyContent(FlexAlign.Start)
            .width('100%')

            // News image  
            Image(this.newsDetail.imageUrl)
              .width('100%')
              .aspectRatio(1.78) // 16:9 ratio  
              .objectFit(ImageFit.Cover)

            // News content  
            Text(this.newsDetail.content)
              .fontSize(16)
              .lineHeight(24)
              .width('100%')

            // Action buttons  
            Row({ space: 20 }) {
              Button(this.isFavorite ? 'Bookmarked' : 'Bookmark')
                .onClick(() => this.toggleFavorite())

              Button('Share')
                .onClick(() => {
                  // Implement share functionality  
                })
            }
            .margin({ top: 20, bottom: 20 })

            // Comments section  
            Text('Comments')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 10 })

            // Comment input  
            if (this.currentUser) {
              Row() {
                TextInput({ text: this.newComment, placeholder: 'Write your comment...' })
                  .onChange((value: string) => {
                    this.newComment = value;
                  })
                  .layoutWeight(1)

                Button('Send')
                  .onClick(() => this.submitComment())
              }
              .width('100%')
              .height(40)
            }

            // Comments list  
            ForEach(this.comments, (comment: Comment) => {
              CommentItem({ comment })
            }, (comment: Comment) => comment.id)
          }
          .padding(15)
        }
      } else {
        LoadingIndicator()
          .width(50)
          .height(50)
      }
    }
    .width('100%')
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Personalized Recommendations and Data Analysis

5.1 User Behavior-Based Recommendation System

class RecommendationService {
  private cloudDB: agconnect.cloudDB.CloudDBZone;

  constructor() {
    const config = {
      name: 'RecommendationZone',
      persistenceEnabled: true
    };
    this.cloudDB = agconnect.cloudDB.CloudDBZoneWrapper.openCloudDBZone(config);
  }

  // Record user viewing behavior  
  async recordView(newsId: string, userId: string, duration: number) {
    const viewRecord = {
      id: `${userId}_${newsId}_${Date.now()}`,
      newsId,
      userId,
      duration,
      timestamp: new Date().getTime()
    };
    await this.cloudDB.executeUpsert('ViewRecord', [viewRecord]);
  }

  // Get personalized recommendations  
  async getPersonalizedRecommendations(userId: string, count: number = 10): Promise<NewsItem[]> {
    // Can integrate more sophisticated recommendation algorithms here  
    const query = agconnect.cloudDB.CloudDBZoneQuery.where('ViewRecord')
      .equalTo('userId', userId)
      .orderByDesc('timestamp')
      .limit(5);

    const recentViews = await this.cloudDB.executeQuery(query);
    const viewedCategories = new Set<string>();

    recentViews.forEach(view => {
      viewedCategories.add(view.news.category);
    });

    // Simulate category-based recommendations  
    return await this.newsService.fetchNewsByCategories(
      Array.from(viewedCategories), 
      count
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

5.2 Integrating AppGallery Connect Analytics

import '@hw-agconnect/analytics-ohos';

class AnalyticsService {
  private analytics = agconnect.analytics();

  // Record news view event  
  logNewsView(newsId: string, category: string, duration: number) {
    this.analytics.logEvent('news_view', {
      news_id: newsId,
      category: category,
      duration: duration,
      timestamp: new Date().getTime()
    });
  }

  // Record user interaction event  
  logUserInteraction(eventType: string, newsId: string) {
    this.analytics.logEvent(eventType, {
      news_id: newsId,
      user_id: agconnect.auth().currentUser?.uid || 'anonymous',
      timestamp: new Date().getTime()
    });
  }

  // Set user properties  
  setUserProperties(properties: Record<string, string>) {
    this.analytics.setUserProperties(properties);
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Application Publishing and Continuous Optimization

6.1 Application Packaging and Publishing

  1. Configure release signing: Generate release certificates in AppGallery Connect
  2. Build release version: In DevEco Studio, execute "Build > Build HAP(s)/APP(s) > Build APP"
  3. Submit for review: Upload the generated .app file to AppGallery Connect

6.2 Performance Optimization Recommendations

  1. Image loading optimization:
// Use image caching and lazy loading  
Image(this.newsItem.imageUrl)
  .width('100%')
  .height(200)
  .objectFit(ImageFit.Cover)
  .syncLoad(false) // Enable async loading  
  .cached(true)    // Enable caching  
Enter fullscreen mode Exit fullscreen mode
  1. List rendering optimization:
List() {
  ForEach(this.newsList, (item: NewsItem) => {
    ListItem() {
      NewsCard({ newsItem: item })
    }
  }, (item: NewsItem) => item.id)
}
.recycle(true) // Enable list item recycling  
.edgeEffect(EdgeEffect.None) // Disable edge effects for better performance  
Enter fullscreen mode Exit fullscreen mode
  1. Network request optimization:
// Use caching strategy to reduce duplicate requests  
async fetchNewsList(category: string, page: number): Promise<NewsItem[]> {
  let cacheKey = `news_${category}_${page}`;
  let cachedData = this.cache.get(cacheKey);

  if (cachedData && Date.now() - cachedData.timestamp < 300000) { // 5-minute cache  
    return cachedData.data;
  }

  // Actual network request...  
}
Enter fullscreen mode Exit fullscreen mode

6.3 User Feedback and Iteration

  1. Integrate AppGallery Connect feedback service:
import '@hw-agconnect/feedback-ohos';

class FeedbackService {
  private feedback = agconnect.feedback();

  // Submit user feedback  
  async submitFeedback(content: string, contact?: string) {
    await this.feedback.submit({
      content: content,
      contact: contact || '',
      type: 'suggestion'
    });
  }

  // Check for new replies  
  async checkForReplies() {
    const replies = await this.feedback.getReplies();
    return replies.filter(reply => !reply.isRead);
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Implement A/B testing with Remote Configuration:
import '@hw-agconnect/remoteconfig-ohos';

class ABTestService {
  private remoteConfig = agconnect.remoteConfig();

  constructor() {
    this.remoteConfig.applyDefault({
      'homepage_layout': 'list',
      'detail_font_size': '16',
      'enable_dark_mode': true
    });
  }

  async fetchConfig() {
    await this.remoteConfig.fetch(3600); // 1-hour expiration  
    await this.remoteConfig.apply();
  }

  getHomepageLayout(): string {
    return this.remoteConfig.getValue('homepage_layout').asString();
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Conclusion and Future Prospects

Through this tutorial, we have fully implemented a news application based on HarmonyOS Next, covering the entire process from data acquisition and user interaction to data analysis. HarmonyOS Next, combined with the rich services provided by AppGallery Connect, offers powerful support for developers to build high-quality applications.

Future Expansion Directions:

  1. Incorporate HarmonyOS's distributed capabilities for cross-device news reading experiences
  2. Add video news support using HarmonyOS's multimedia capabilities
  3. Develop smartwatch applications to expand usage scenarios
  4. Integrate smarter recommendation algorithms to enhance user experience

News application development is an ongoing iterative process. It is recommended to regularly analyze user behavior through AppGallery Connect's analytics data to continuously optimize product features and user experience.

Top comments (0)