鸿蒙HarmonyOS分布式项目实战:分布式点餐系统

160 篇文章 0 订阅
150 篇文章 0 订阅

本项目可以使用在聚餐时餐馆点餐或者外卖拼单,由一个人打开菜单页分享给同桌所有人员。

所有人员可以在自己手机选择自己需要点的菜,会在同一时间同步到所有人手机,每个人都可以看到他人点的菜单,所有人同时维护一个菜单。

不需要像传统的方式传菜单点菜,服务员手动登记;或者由一个人扫码点菜,其他人均把需要的菜报给点菜人员。

此项目旨在帮助开发者快速了解 HarmonyOS 应用开发、JS-JAVA 通信、跨设备调用 PA 以及分布式数据库的使用。

搭建 HarmonyOS 环境

①安装 DevEco Studio,详情请参考 DevEco Studio下载:

https://developer.harmonyos.com/cn/develop/deveco-studio

②设置 DevEco Studio 开发环境,DevEco Studio 开发环境需要依赖于网络环境。

可以根据如下两种情况来配置开发环境:

  • 如果可以直接访问 Internet,只需进行下载 HarmonyOS SDK 操作。

  • 如果网络不能直接访问 Internet,需要通过代理服务器才可以访问,请参考配置开发环境。

https://developer.harmonyos.com/cn/docs/documentation/doc-guides/environment_config-0000001052902427

③本程序需要在真机运行,需要提前申请证书。

准备密钥和证书请求文件:

https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ide_debug_device-0000001053822404

申请调试证书:

https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-help-harmonyos-debugapp-0000001172419675

代码结构解读

本教程我们只是对核心代码进行讲解,您可以在最后的参考中下载完整代码,首先来介绍下整个工程的代码结构:

图片

①Java-data:封装菜品实体类和部分字符串常量。

②Java-service:SharePageServiceAbility 供 js 与 java 通信的 PA,此服务中跨设备调用 FA(MainAbility);DBInternalAbility 供 js 与 java 通信的 PA,js 通过此服务调用 java 的分布式数据库工具类。

③Java-utils:封装了数据库操作的工具类。

④Js-common:components 存放公共组件,imgs 存放业务图片,json 存放模拟数据。

⑤Js-pages:detail 商家菜品列表展示页面,index 商家列表展示页面,shoppingCart 结账页。

⑥config.json:配置文件。

跨设备打开点餐页面

①权限申请

本程序开发需要申请以下 4 个权限:

  • ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息。

  • ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换。

  • ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化。

  • ohos.permission.GET_BUNDLE_INFO:用于查询其他应用的信息。

在 config.json 中增加下面权限申请代码:

"reqPermissions": [
  {
    "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO"
  },
  {
    "name": "ohos.permission.DISTRIBUTED_DATASYNC"
  },
  {
    "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"
  },
  {
    "name": "ohos.permission.GET_BUNDLE_INFO"
  }
]

在 MainAbility.java 的 onStart() 中申请权限,主要代码如下:

private static final String PERMISSION_DATASYNC = "ohos.permission.DISTRIBUTED_DATASYNC";
private static final int MY_PERMISSION_REQUEST_CODE = 1;
private void requestPermission() {
    if (verifySelfPermission(PERMISSION_DATASYNC) != IBundleManager.PERMISSION_GRANTED) {
        if (canRequestPermission(PERMISSION_DATASYNC)) {
            requestPermissionsFromUser(new String[] {PERMISSION_DATASYNC}, MY_PERMISSION_REQUEST_CODE);
        }
    }
}
②FA(JS API)调用 PA(Java API)

detail 商家菜品列表展示页面(点餐页),点击头部分享按钮,调用 SharePageServiceAbility 与 java 通信:

const ABILITY_TYPE_EXTERNAL = 0;
const ABILITY_TYPE_INTERNAL = 1;// syncOption(Optional, default sync): 0-Sync; 1-Async
const ACTION_SYNC = 0;
const DISTRIBUTE_PAGE = 1000;

shareToOthers: async function(){
       var actionData = {};
       actionData.restaurantId = this.restaurantId;

       var action = {};
       action.bundleName = 'com.example.ordering';
       action.abilityName = 'com.example.ordering.service.SharePageServiceAbility';
       action.messageCode = DISTRIBUTE_PAGE;
       action.data = actionData;
       action.abilityType = ABILITY_TYPE_EXTERNAL;
       action.syncOption = ACTION_SYNC;

       var result = await FeatureAbility.callAbility(action);
   }

③发现设备和打开迁移设备页面

SharePageServiceAbility 中,打开所有同桌人员的 MainAbility 页面:

List<DeviceInfo> deviceInfoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);                 if(deviceInfoList != null && deviceInfoList.size()>0){
  for (DeviceInfo info: deviceInfoList){
    Intent intent = new Intent();
    Operation operation = new Intent.OperationBuilder()
        .withDeviceId(info.getDeviceId())
        .withBundleName("com.example.ordering")
        .withAbilityName("com.example.ordering.MainAbility")
        .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
        .build();
    intent.setParam(Constant.INIT_PAGE_PARAM,Constant.PAGE_DETAIL);
    intent.setParam(Constant.INIT_RESTAURANTID,param.getRestaurantId());
    intent.setOperation(operation);
    startAbility(intent);
    }
   }

④如何路由到菜单页

MainAbility.java 初始化 start 方法中,将餐馆 id、需要路由的页面传给 js 页面:

String init_page = intent.getStringParam(Constant.INIT_PAGE_PARAM);
String init_restaurantId = intent.getStringParam(Constant.INIT_RESTAURANTID);
if(init_page !=null && !init_page.isEmpty()) {
   IntentParams params = new IntentParams();
   params.setParam(Constant.INIT_PAGE_PARAM, Constant.PAGE_DETAIL);
   params.setParam(Constant.INIT_RESTAURANTID, init_restaurantId);
   setPageParams(null, params);
}
DBInternalAbility.register(this);
setInstanceName("default");

js-default 模块首页,路由到对应的点餐 detail 页面:

onReady(){
  if(this.init_page == "detail"){
     router.push({uri:'pages/detail/detail',params:{restaurantId:this.init_restaurantId}});
    }
 }

分布式数据库数据处理

①权限申请

同上一步的权限申请,无需增加权限申请。

②创建分布式数据库(DbHelper)

要创建分布式数据库,首先要创建分布式数据库管理器实例 KvManager,方法如下:

private KvManager createManager() {
    KvManager kvmanager = null;
    try {
        KvManagerConfig config = new KvManagerConfig(context);
        kvmanager = KvManagerFactory.getInstance().createKvManager(config);
    } catch (KvStoreException exception) {
        HiLog.info(LABEL_LOG, "some exception happen");
    }
    return kvmanager;
}

KvManager 创建成功后,借助 KvManager 创建 SINGLE_VERSION 分布式数据库,方法如下:

private SingleKvStore createDb(KvManager kvmanager) {
        if(kvmanager == null) return null;
        SingleKvStore kvStore = null;
        try {
            Options options = new Options();
            options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
            kvStore = kvmanager.getKvStore(options, STORE_ID);
        } catch (KvStoreException exception) {
            HiLog.info(LABEL_LOG, "some exception happen");
        }
        return kvStore;
    }

SINGLE_VERSION 分布式数据库是指数据在本地保存是以单个 KV 条目为单位的方式保存,对每个 Key 最多只保存一个条目项。

当数据在本地被用户修改时,不管它是否已经被同步出去,均直接在这个条目上进行修改。

最后是订阅分布式数据库中数据变化,方法如下:

private void subscribeDb(SingleKvStore kvStore) {
    if(kvStore == null) return;
    KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient();
    kvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_REMOTE, kvStoreObserverClient);
}

分布式数据库支持订阅远端和本地的数据变化,示例代码中订阅的方式为订阅远端,订阅本地的参数为 SUBSCRIBE_TYPE_LOCAL;同时还支持订阅全部,参数为 SUBSCRIBE_TYPE_ALL。

③数据查询、插入和删除

数据插入:先构造分布式数据库的 Key(键)和 Value(值),通过 putString 方法将数据写入到数据库中。

具体示例如下:

private void writeData(String key, String value) { 
if (key == null || key.isEmpty() || value == null || value.isEmpty()) { 
     return; 
 } 
 singleKvStore.putString(key, value); 
}

public void writeData(String name, int count, int deskNum, String restaurantId,String dishId) {
    if(kvStore == null) return;
    if (name == null || name.isEmpty()) {
        return;
    }
    kvStore.putString(name+restaurantId, name+";"+count+";"+deskNum+";"+Dish.NO_VALUE+";"+restaurantId+";"+dishId);
}

数据查询:根据Key(键)来进行查询,如果指定 Key,则会查询出对应 Key 的数据;如果不指定 Key,既为空,则查询出所有数据。

查询示例代码如下(除查询外,还有部分场景业务代码):

public String queryData(String restaurantId) {
    this.restaurantId = restaurantId;
    if(kvStore == null) return null;
    List<Entry> entryList = kvStore.getEntries("");
    String json = "[";
    try {
        for (Entry entry : entryList) {
            String name = entry.getKey();
            String[] values = entry.getValue().getString().split(";");
            if(restaurantId == null || restaurantId.equals(values[4])) {
                json += "{" +
                        "\"name\":\"" + values[0] + "\"," +
                        "\"count\":" + values[1] + "," +
                        "\"deskNum\":" + values[2] + "," +
                        "\"address\":\"" + values[3] + "\"," +
                        "\"restaurantId\":\"" + values[4] +"\"," +
                        "\"id\":\"" + values[5] +
                        "\"},";
            }
        }
    } catch (KvStoreException exception) {
        HiLog.info(LABEL_LOG,"the value must be String");
    }

    json += "]";
    if(json.contains(",]")){
        json = json.replace(",]","]");
    }

    return json;

}

数据删除:删除操作可以直接调用 delete() 方法,但是需要传递事先定义好的 key(键)。

示例代码如下:

public void deleteData(String key) {
   if(kvStore == null) return;
   if (key.isEmpty()) {
      return;
   }
   kvStore.delete(key+restaurantId);
   HiLog.info(LABEL_LOG,  "deleteContact key= " + key);
 }

数据库删除:删除操作可以直接调用 deleteKvStore() 方法,但是需要传递事先定义好的 STORE_ID 参数。

示例代码如下:

④分布式数据库的同步

在进行数据同步之前,首先需要先获取当前组网环境中的设备列表,然后指定同步方式(PULL_ONLY,PUSH_ONLY,PUSH_PULL)进行同步。

以 PUSH_PULL 方式为例,示例代码如下:

public void syncData(String restaurantId) {
        this.restaurantId = restaurantId;
        if(kvmanager == null || kvStore == null) return;
        List<DeviceInfo> deviceInfoList = kvmanager.getConnectedDevicesInfo(DeviceFilterStrategy.NO_FILTER);
        List<String> deviceIdList = new ArrayList<>();
        for (DeviceInfo deviceInfo : deviceInfoList) {
            deviceIdList.add(deviceInfo.getId());
        }
        HiLog.info(LABEL_LOG, "device size= " + deviceIdList.size());
        if (deviceIdList.size() == 0) {
            String result = queryData(this.restaurantId);
            if(kvStoreLishner != null) {
                kvStoreLishner.updataUI(result);
            }
            return;
        }
        kvStore.registerSyncCallback(new SyncCallback() {
            @Override
            public void syncCompleted(Map<String, Integer> map) {
                String result = queryData(getRestaurantId());
                if(kvStoreLishner != null) {
                   kvStoreLishner.updataUI(result);
                }
                kvStore.unRegisterSyncCallback();
            }
        });
        kvStore.sync(deviceIdList, SyncMode.PUSH_PULL);
    }

以上代码除数据同步外,还有部分场景业务代码。

点餐页面数据同步

①跨设备 FA 页面打开,初始数据同步

初始打开点单 detail 页,同步组网内其他设备已经点好的菜单,在生命周期 onShow 方法中调用 subscribeInternal 方法订阅 PA-DBInternalAbility。

生命中期 onHide 方法中调用 unsubscribeInternal 方法取消订阅 PA-DBInternalAbility。

具体代码如下:

subscribeInternal: async function() {
       var that = this;
       var actionData = {};
       actionData.restaurantId = this.restaurantId;
       var action = {};
       action.bundleName = 'com.example.ordering';
       action.abilityName = 'com.example.ordering.service.DBInternalAbility';
       action.messageCode = ACTION_MESSAGE_CODE_SUBSCRIBE;
       action.data = actionData;
       action.abilityType = ABILITY_TYPE_INTERNAL;
       action.syncOption = ACTION_SYNC;
       var result = await FeatureAbility.subscribeAbilityEvent(action, function (callbackData) {
           var callbackJson = JSON.parse(callbackData);
           that.queryData = JSON.parse(callbackJson.data.abilityEvent);
           that.initDetailAndCart();
       });
   },

unsubscribeInternal: async function() {
       var action = {};
       action.bundleName = 'com.example.ordering';
       action.abilityName = 'com.example.ordering.service.DBInternalAbility';
       action.messageCode = ACTION_MESSAGE_CODE_UNSUBSCRIBE;
       action.abilityType = ABILITY_TYPE_INTERNAL;
       action.syncOption = ACTION_SYNC;

       var result = await FeatureAbility.unsubscribeAbilityEvent(action);
   }

PA-DBInternalAbility 在 onRemoteRequest 方法中对 js 传输过来的订阅与取消订阅进行两种事件进行处理。

具体代码如下:

case SUBSCRIBE: {
   remoteObjectHandler = data.readRemoteObject();
   String zsonStr = data.readString();
   Dish param = new Dish();
   try {
     param = ZSONObject.stringToClass(zsonStr, Dish.class);
   } catch (RuntimeException e) {
     HiLog.error(LABEL, "convert failed.");
   }
   dbHelper.syncData(param.getRestaurantId());
   break;
 }
// 取消订阅,置空对端的remoteHandler
case UNSUBSCRIBE: {
   remoteObjectHandler = null;
   break;
 }

dbHelper.syncData 数据库同步方法已经在上面进行介绍,同步方法中数据库同步之后,会查询分布式数据库数据通过 kvStoreLishner.updataUI(result) 更新界面数据。

updataUI 具体实现代码如下:

public void updataUI(String result) {
  try {
    MessageParcel data = MessageParcel.obtain();
    MessageParcel reply = MessageParcel.obtain();
    MessageOption option = new MessageOption();
    Map<String, Object> zsonEvent = new HashMap<String, Object>();
    zsonEvent.put("abilityEvent", result);
    data.writeString(ZSONObject.toZSONString(zsonEvent));
    if(remoteObjectHandler != null)
      remoteObjectHandler.sendRequest(100, data, reply, option);
      reply.reclaim();
      data.reclaim();
   } catch (RemoteException e) {

   }
 }

PA-DBInternalAbility 需要在 MainAbility 中的 onStart 方法中调用 DBInternalAbility.register 进行注册。

onStop 方法中调用 DBInternalAbility.unregister 进行取消注册,具体注册、取消注册代码如下:

/**
    * Internal ability registration.
    */
   public static void register(AbilityContext abilityContext) {
       instance = new DBInternalAbility();
       instance.onRegister(abilityContext);
   }

   private void onRegister(AbilityContext abilityContext) {
       dbHelper = new DbHelper(abilityContext,kvStoreLishner);
       dbHelper.initDbManager();
       this.abilityContext = abilityContext;
       this.setInternalAbilityHandler((code, data, reply, option) -> {
           return this.onRemoteRequest(code, data, reply, option);
       });
   }

   /**
    * Internal ability unregistration.
    */
   public static void unregister() {
       instance.onUnregister();
   }

   private void onUnregister() {
       dbHelper.deleteDb();
       abilityContext = null;
       this.setInternalAbilityHandler(null);
   }
②某个设备 FA 页面操作,其他设备 FA 页面数据同步

因为在创建数据库时,我们对分布式数据库的变化进行了订阅,所以每次数据库变动均会调用 kvStoreLishner.updataUI 更新界面数据。

具体代码如下:

public void initDbManager() {
       kvmanager = createManager();
       kvStore = createDb(kvmanager);
       subscribeDb(kvStore);
   }

/**
    * 最后是订阅分布式数据库中数据变化
    */
   private void subscribeDb(SingleKvStore kvStore) {
       if(kvStore == null) return;
       KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient();
       kvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_REMOTE, kvStoreObserverClient);
   }

/**
    * Receive database messages
    */
   private class KvStoreObserverClient implements KvStoreObserver {

       @Override
       public void onChange(ChangeNotification notification) {
           String result = queryData(getRestaurantId());
           if(kvStoreLishner != null) {
                 kvStoreLishner.updataUI(result);
           }
       }
   }

   public interface KvStoreLishner{
       void updataUI(String result);
   }

最终实现效果

最终实现效果如下图:

图片

回顾和总结

本篇通过一个分布式点餐系统,完整的为开发者们介绍了多人在线点餐数据共享案例,旨在帮助开发者快速了解 HarmonyOS 应用开发、JS-JAVA 通信、获取可迁移设备、打开迁移设备的页面、分布式数据库使用。

我们通过拆解步骤的方式详细为开发者介绍了如何在多台设备之间进行数据共享、页面分享,这是开发者需要重点学习和掌握的知识点。

最后

如果你想成为一名鸿蒙开发者,以下这些资料将是十分优质且有价值,让你的鸿蒙开发之路事半功倍!相对于网上那些碎片化的知识内容,这份学习资料的知识点更加系统化,更容易理解和记忆。

内容包含了:【OpenHarmony多媒体技术、Stage模型、ArkUI多端部署、分布式应用开发、音频、视频、WebGL、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战】等技术知识点。

鸿蒙Next全套VIP学习资料←点击领取!(安全链接,放心点击

1.鸿蒙核心技术学习路线

2.大厂面试必问面试题

3.鸿蒙南向开发技术

 4.鸿蒙APP开发必备

 5.HarmonyOS Next 最新全套视频教程

 6.鸿蒙生态应用开发白皮书V2.0PDF

这份全套完整版的学习资料已经全部打包好,朋友们如果需要可以点击鸿蒙Next全套VIP资料:免费领取(安全链接,放心点击

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值