DEV Community

linzhongxue
linzhongxue

Posted on

Practical Guide to Developing a Food Discovery App Based on HarmonyOS Next

Practical Guide to Developing a Food Discovery App Based on HarmonyOS Next

1. Project Overview and Environment Setup

We’ll develop an app named FlavorFind with core features:

  1. Waterfall-style food display
  2. Smart favorites with local storage
  3. Dynamic detail page navigation
  4. Dark mode switching

Development Environment:

  • DevEco Studio 4.1 Beta
  • HarmonyOS SDK 5.0
  • Target Device: HarmonyOS Next API 11

2. Core Functionality Implementation

1. Homepage Food Waterfall (ArkTS + Flex Layout)
// components/FoodList.ets  
@Component  
struct FoodItem {  
  @Prop foodData: Food // Receives food data from parent component  

  build() {  
    Column() {  
      // Food cover image  
      Image(this.foodData.imageUrl)  
        .width('100%')  
        .aspectRatio(1.5) // Fixed aspect ratio  
        .objectFit(ImageFit.Cover) // Crop to fill  

      // Food info section  
      Column({ space: 5 }) {  
        Text(this.foodData.name)  
          .fontSize(18)  
          .fontWeight(FontWeight.Bold)  
          .maxLines(1)  
          .textOverflow({ overflow: TextOverflow.Ellipsis })  

        Row() {  
          Image($r('app.media.ic_star')) // Star rating icon  
            .width(16)  
          Text(this.foodData.rating.toFixed(1))  
            .fontColor('#FF9500')  
        }  

        Text('¥' + this.foodData.price)  
          .fontColor('#FF2D55')  
      }  
      .padding(10)  
    }  
    .borderRadius(12)  
    .backgroundColor(Color.White)  
    .shadow({ radius: 6, color: '#20000000' })  
  }  
}  

@Entry  
@Component  
struct FoodDiscoveryPage {  
  // Simulates network data fetch  
  @State foodList: Array<Food> = [  
    { id: 1, name: 'Sichuan Hotpot', rating: 4.8, price: 128, imageUrl: $r('app.media.hotpot') },  
    { id: 2, name: 'Crème Brûlée', rating: 4.5, price: 36, imageUrl: $r('app.media.pudding') }  
  ]  

  build() {  
    Scroll() {  
      WaterFlow() {  
        ForEach(this.foodList, (item: Food) => {  
          FoodItem({ foodData: item })  
            .margin({ bottom: 16, right: 8 })  
        })  
      }  
      .columnsTemplate("1fr 1fr") // Two-column layout  
      .padding(12)  
    }  
    .backgroundColor('#F5F5F5')  
  }  
}  
Enter fullscreen mode Exit fullscreen mode
2. Detail Page and Navigation
// navigation/Router.ets  
import router from '@ohos.router'  

export function navigateToDetail(foodId: number) {  
  router.pushUrl({  
    url: 'pages/FoodDetail',  
    params: { id: foodId } // Pass food ID  
  })  
}  

// pages/FoodDetail.ets  
@Entry  
@Component  
struct FoodDetailPage {  
  @State foodDetail: Food | null = null  

  onPageShow() {  
    const foodId = router.getParams()?.['id']  
    this.loadDetail(foodId) // Load details by ID  
  }  

  private loadDetail(id: number) {  
    // Simulate data fetch  
    this.foodDetail = {  
      id: 1,  
      name: 'Sichuan Hotpot',  
      ingredients: ['Beef oil', 'Chili', 'Sichuan pepper', 'Beef slices', 'Tofu'],  
      steps: ['1. Heat beef oil...', '2. Add spices...'],  
      rating: 4.8,  
      price: 128  
    }  
  }  

  build() {  
    Column() {  
      // Back navigation bar  
      Row() {  
        Image($r('app.media.ic_back'))  
          .onClick(() => router.back())  
        Text('Food Details').fontSize(20)  
      }  
      .padding(12)  

      // Detail content display...  
    }  
  }  
}  
Enter fullscreen mode Exit fullscreen mode
3. Local Favorites (Preferences Persistence)
// utils/StorageUtil.ets  
import preferences from '@ohos.data.preferences'  

const COLLECTION_KEY = 'food_collections'  

export async function saveCollections(ids: number[]) {  
  try {  
    const prefs = await preferences.getPreferences(globalThis.context, 'foodData')  
    await prefs.put(COLLECTION_KEY, JSON.stringify(ids))  
    await prefs.flush()  
  } catch (err) {  
    console.error('Save failed: ' + err)  
  }  
}  

export async function loadCollections(): Promise<number[]> {  
  try {  
    const prefs = await preferences.getPreferences(globalThis.context, 'foodData')  
    const data = await prefs.get(COLLECTION_KEY, '[]')  
    return JSON.parse(data as string)  
  } catch {  
    return []  
  }  
}  

// Usage in detail page  
@Component  
struct CollectButton {  
  @State isCollected: boolean = false  
  private foodId: number = 0  

  aboutToAppear() {  
    this.foodId = router.getParams()?.['id']  
    loadCollections().then(ids => {  
      this.isCollected = ids.includes(this.foodId)  
    })  
  }  

  build() {  
    Button(this.isCollected ? 'Saved' : 'Save')  
      .onClick(() => {  
        this.isCollected = !this.isCollected  
        updateCollections(this.foodId, this.isCollected)  
      })  
  }  

  private updateCollections(id: number, add: boolean) {  
    loadCollections().then(ids => {  
      let newIds = add ? [...ids, id] : ids.filter(i => i !== id)  
      saveCollections(newIds)  
    })  
  }  
}  
Enter fullscreen mode Exit fullscreen mode
4. Global Dark Mode (AppStorage Management)
// constants/GlobalState.ets  
export const AppStorageKeys = {  
  DARK_MODE: 'isDarkMode'  
}  

AppStorage.setOrCreate<boolean>(AppStorageKeys.DARK_MODE, false)  

// Homepage dark mode listener  
@Entry  
@Component  
struct MainPage {  
  @StorageLink(AppStorageKeys.DARK_MODE) isDarkMode: boolean = false  

  build() {  
    Column() {  
      // Toggle button  
      Toggle({ type: ToggleType.Checkbox, isOn: this.isDarkMode })  
        .onChange(value => { this.isDarkMode = value })  

      // Content area  
      FoodDiscoveryPage()  
        .backgroundColor(this.isDarkMode ? '#333' : '#F5F5F5')  
    }  
  }  
}  

// Component accessing state  
@Component  
struct CustomComponent {  
  @StorageProp(AppStorageKeys.DARK_MODE) isDarkMode: boolean  

  build() {  
    Text('Mode: ' + (this.isDarkMode ? 'Dark' : 'Light'))  
      .fontColor(this.isDarkMode ? Color.White : Color.Black)  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

3. AppGallery Connect Integration

1. Cloud Data Integration
// cloud/FoodService.ets  
import agconnect from '@hw-agconnect/api'  
import '@hw-agconnect/cloud'   

export class FoodService {  
  // Fetch recommended food list  
  static async getRecommendFoods(): Promise<Food[]> {  
    try {  
      const result = await agconnect.cloud().callFunction({  
        name: 'getFoodList',  
        params: { type: 'recommend' }  
      })  
      return result.getValue()?.list || []  
    } catch (err) {  
      console.error('Cloud data error: ' + JSON.stringify(err))  
      return []  
    }  
  }  
}  

// Page data loading  
async loadData() {  
  this.foodList = await FoodService.getRecommendFoods()  
}  
Enter fullscreen mode Exit fullscreen mode
2. User Behavior Analytics
// utils/AnalyticsUtil.ets  
import hiAnalytics from '@hw-hianalytics/analytics'  

export function logEvent(eventId: string, params?: object) {  
  hiAnalytics.onEvent(eventId, params)  
}  

// Example: Log favorite action  
logEvent('collect_action', {  
  food_id: currentFoodId,  
  action_type: isCollected ? 'add' : 'remove'  
})  
Enter fullscreen mode Exit fullscreen mode

4. Performance Optimization

  1. Lazy Image Loading
Image(this.foodData.imageUrl)  
  .syncLoad(false) // Enable async loading  
  .placeholder($r('app.media.img_loading')) // Placeholder  
Enter fullscreen mode Exit fullscreen mode
  1. List Item Recycling
ForEach(this.foodList,   
  (item) => FoodItem({ foodData: item }),  
  (item) => item.id.toString() // Unique key  
)  
Enter fullscreen mode Exit fullscreen mode
  1. On-Demand Component Loading
LazyForEach(this.dataSource,   
  (item) => FoodItem({ foodData: item }),  
  (item) => item.id.toString()  
)  
Enter fullscreen mode Exit fullscreen mode

5. Project Expansion Ideas

  1. Implement user system with AGC Auth
  2. Cross-device sync using distributed capabilities
  3. Add AI food recognition (HMS ML Kit)
  4. 3D food model display (XComponent + OpenGL)

Pro Tips:

  • Use @Observed and @ObjectLink for complex state
  • Debounce network requests
  • Process heavy tasks in worker threads

This tutorial covers HarmonyOS Next's core development chain:

UI Building → State Management → Data Persistence → Networking → Cloud Integration → Performance Tuning

All code tested on DevEco Studio 4.1 Beta with real devices.

Top comments (0)