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:
- Waterfall-style food display
- Smart favorites with local storage
- Dynamic detail page navigation
- 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')
}
}
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...
}
}
}
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)
})
}
}
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)
}
}
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()
}
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'
})
4. Performance Optimization
- Lazy Image Loading
Image(this.foodData.imageUrl)
.syncLoad(false) // Enable async loading
.placeholder($r('app.media.img_loading')) // Placeholder
- List Item Recycling
ForEach(this.foodList,
(item) => FoodItem({ foodData: item }),
(item) => item.id.toString() // Unique key
)
- On-Demand Component Loading
LazyForEach(this.dataSource,
(item) => FoodItem({ foodData: item }),
(item) => item.id.toString()
)
5. Project Expansion Ideas
- Implement user system with AGC Auth
- Cross-device sync using distributed capabilities
- Add AI food recognition (HMS ML Kit)
- 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)