Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Distributed Restaurant Ordering App with HarmonyOS

Tech 1

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.