一、项目介绍

    本项目可以使用在聚餐时餐馆点餐或者外卖拼单,由一个人打开菜单页分享给同桌所有人员,所有人员可以在自己手机选择自己需要点的菜,会在同一时间同步到所有人手机,每个人都可以看到他人点的菜单,所有人同时维护一个菜单。不需要像传统的方式传菜单点菜,服务员手动登记;或者由一个人扫码点菜,其他人均把需要的菜报给点菜人员。
    此项目旨在帮助开发者快速了解HarmonyOS应用开发、JS-JAVA通信、跨设备调用PA以及分布式数据库的使用。

二、搭建HarmonyOS环境

1、安装DevEco Studio,详情请参考DevEco Studio下载
2、设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,可以根据如下两种情况来配置开发环境:
    1)如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作
    2)如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
3、本程序需要在真机运行,需要提前申请证书:
    1)准备密钥和证书请求文件
    2)申请调试证书

三、代码结构解读

本教程我们只是对核心代码进行讲解,您可以在最后的参考中下载完整代码,首先来介绍下整个工程的代码结构:
file_structure.png
    1、Java-data:封装菜品实体类和部分字符串常量
    2、Java-service:SharePageServiceAbility供js与java通信的PA,此服务中跨设备调用FA(MainAbility);DBInternalAbility供js与java通信的PA,js通过此服务调用java的分布式数据库工具类。
    3、Java-utils:封装了数据库操作的工具类。
    4、Js-common:components存放公共组件,imgs存放业务图片,json存放模拟数据
    5、Js-pages:detail商家菜品列表展示页面,index商家列表展示页面,shoppingCart结账页
    6、config.json:配置文件

四、跨设备打开点餐页面

1.权限申请

本程序开发需要申请以下4个权限:
    ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息。
    ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换。
    ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化。
    ohos.permission.GET_BUNDLE_INFO:用于查询其他应用的信息。

1)在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"
  }
]

2)在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);
		}
	}
}
2.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);
   }
3.发现设备和打开迁移设备页面

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);
    }
   }
4.如何路由到菜单页

1)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");

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

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

五、分布式数据库数据处理

1.权限申请

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

2.创建分布式数据库(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。

3.数据查询、插入和删除

1)数据插入
先构造分布式数据库的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);
}

2)数据查询
根据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;

}

3)数据删除
删除操作可以直接调用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);
 }

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

public void deleteDb(){
	if(kvmanager == null) return;
	kvmanager.closeKvStore(kvStore);
	kvmanager.deleteKvStore(STORE_ID);
}
4.分布式数据库的同步

在进行数据同步之前,首先需要先获取当前组网环境中的设备列表,然后指定同步方式(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);
    }

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

六、点餐页面数据同步

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

1)初始打开点单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);
   }

2)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;
 }

3)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) {

   }
 }

4)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);
   }
2.某个设备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);
   }

七、最终实现效果

distribute_demo.gif

八、回顾和总结

    本篇通过一个分布式点餐系统,完整的为您介绍了多人在线点餐数据共享案例,旨在帮助您快速了解HarmonyOS应用开发、JS-JAVA通信、获取可迁移设备、打开迁移设备的页面、分布式数据库使用。我们通过拆解步骤的方式详细为您介绍了如何在多台设备之间进行数据共享、页面分享,这是您需要重点学习和掌握的知识点。
    特别说明,运行时需要至少两台手机处于同一个分布式网络中,可以通过操作如下配置实现:
    1)所有设备接入同一网络;
    2)所有设备登录相同华为帐号;
    3)所有设备上开启"设置->更多连接->多设备协同 ";
    4)所有设备上开启蓝牙功能

九、代码参考

https://gitee.com/chinasoft6_ohos/distributed-ordering

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz

21_9.jpg