Building a Distributed Restaurant Ordering App with HarmonyOS
This example demonstrates a multi-user restaurant ordreing application leveraging HarmonyOS’s distributed data capabilities. Users on different devices within the same network can collaboratively view and update an order in real time without requiring QR codes or external services.
Project Structure
├─entry
│ └─src
│ └─main
│ │ config.json
│ │
│ ├─ets
│ │ └─MainAbility
│ │ │ app.ets
│ │ │
│ │ ├─model
│ │ │ CommonLog.ets
│ │ │ MenuData.ets
│ │ │ MenuListDistributedData.ets
│ │ │ RemoteDeviceManager.ets
│ │ │ SubmitData.ets
│ │ │
│ │ └─pages
│ │ detailedPage.ets
│ │ index.ets
│ │ menuAccount.ets
│ │
│ └─resources
│ ├─base
│ │ ├─element
│ │ │ string.json
│ │ ├─media
│ │ │ icon.png
│ │ │ icon_add.png
│ │ │ icon_back.png
│ │ │ icon_cart.png
Key Implementation Steps
User Profile Display
The MemberInfo component renders user-specific avatars and names based on the device serial number:
@Component
struct MemberInfo {
@Consume userImg: Resource
@Consume userName: string
aboutToAppear() {
if (deviceInfo.serial === '150100384754463452061bba4c3d670b') {
this.userImg = $r("app.media.icon_user")
this.userName = 'Sunny'
} else {
this.userImg = $r("app.media.icon_user_another")
this.userName = 'Jenny'
}
}
build() {
Flex({ direction: FlexDirection.Column }) {
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
Image(this.userImg)
.width('96lpx')
.height('96lpx')
.margin({ right: '18lpx' })
Text(this.userName)
.fontSize('36lpx')
.fontWeight(FontWeight.Bold)
.flexGrow(1)
Image($r("app.media.icon_share"))
.width('64lpx')
.height('64lpx')
.onClick(() => this.DeviceDialog.open())
}
.padding({ left: '48lpx', right: '48lpx' })
// Loyalty info row...
}
.width('93%')
.height('25%')
.borderRadius('16lpx')
.backgroundColor('#FFFFFF')
.margin({ top: '24lpx', bottom: '32lpx' })
}
}
Menu Listing
The MenuHome component uses category tabs to filter dishes. Each tab loads a corresponding dataset:
@Component
struct MenuHome {
private menuItems: MenuData[]
private titleList = ['Signature', 'Winter Specials', 'Rice Dishes', 'Soups']
@State currentCategory: string = 'Signature'
build() {
Flex({ direction: FlexDirection.Row }) {
// Category sidebar
Flex({ direction: FlexDirection.Column }) {
ForEach(this.titleList, item => {
Text(item)
.padding({ left: '24lpx' })
.backgroundColor(this.currentCategory === item ? '#1A006A3A' : '#FFFFFF')
.height('160lpx')
.onClick(() => {
this.currentCategory = item
this.menuItems = loadMenuByCategory(item)
})
})
}
.width('20%')
// Scrollable dish list
Flex({ direction: FlexDirection.Column }) {
Text(this.currentCategory).fontSize('32lpx').opacity(0.4)
Scroll() {
List() {
ForEach(this.menuItems, item => ListItem() { MenuListItem({ menuItem: item }) })
}
}
}
.width('75%')
.margin({ left: '10lpx' })
}
.height('50%')
}
}
Order Summary and Sync
The TotalInfo footer calculates total price and quantity, then syncs the full order to a distributed database when "Confirm" is tapped:
@Component
struct TotalInfo {
@Consume orderItems: any[]
private remoteDB: MenuListData
build() {
let total = this.orderItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
let count = this.orderItems.reduce((sum, item) => sum + item.quantity, 0)
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
Stack({ alignContent: Alignment.Center }) {
Image($r("app.media.icon_cart"))
.width('96lpx')
.height('96lpx')
Text(count.toString())
.backgroundColor('#F84747')
.borderRadius('30plx')
.fontColor('#FFFFFF')
.width('50lpx')
.height('50lpx')
.margin({ left: '100lpx', bottom: '85lpx' })
}
Text('¥').fontColor('#006A3A')
Text(total.toString()).fontSize('40lpx').fontColor('#006A3A').flexGrow(1)
Text('Confirm')
.width('35%')
.backgroundColor('#F84747')
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.onClick(() => this.remoteDB.putData("menu_list", this.orderItems))
}
.height('10%')
.backgroundColor('#FFFFFF')
}
}
Dish Detail Page
On detailedPage, users select spice level and confirm. The logic checks if the exact dish (name + spice) already exists in the order:
Button('Add to Order')
.onClick(() => {
const existing = this.orderItems.findIndex(
item => item.name === this.dish.name && item.spicy === this.selectedSpice
)
if (existing !== -1) {
this.orderItems[existing].quantity += 1
updateUserCount(this.orderItems[existing], this.currentUser)
} else {
const newItem = { ...this.dish, spicy: this.selectedSpice, quantity: 1 }
initUserCounts(newItem, this.currentUser)
this.orderItems.push(newItem)
}
router.push({ uri: 'pages/index', params: { orderItems: this.orderItems } })
})
Order Confirmation via Distributed Data
The checkout page writes a submitOk flag to a second distributed store (SubmitData). A listener in the entry component detects this change and shows a success dialog:
// In SubmitList.ets
this.remoteSubmitDB.putData("submit", [{ submit: "submitOk" }])
// In entry component
this.submitDB.createManager((data) => {
if (data?.[0]?.submit === "submitOk") {
this.confirmDialog.open()
}
}, "com.distributed.order", "submit")
Device Discovery and Flow
Using RemoteDeviceManager, the app discovers nearby devices via startDeviceDiscovery(), authenticates them, and launches the app on a remote device using featureAbility.startAbility() with the target deviceId.
Distributed Data Setup
Two KVStore instences are created:
- MenuListDistributedData: Syncs the full order list using
storeId = "menu_list_store" - SubmitData: Signals order completion with
storeId = "submit_store"
Both use KVManager.createKVManager() and subscribe to changes via on('dataChange', callback) to enable real-time UI updates acros devices.