满满干货!手把手教你实现基于eTS的分布式计算器


最近收到很多小伙伴反馈,想基于扩展的TS语言(eTS)进行HarmonyOS应用开发,但是不知道代码该从何处写起,从0到1的过程让新手们抓狂。

本期我们将带来“分布式计算器”的开发,帮助大家了解声明式开发范式的UI描述、组件化机制、UI状态管理、渲染控制语法等核心机制和功能。下面我们直接进入正题。

一、整体介绍


分布式计算器可以进行简单的数值计算,并支持远程拉起另一个计算器FA,实现两个FA进行协同计算。

如图1所示,分布式计算器界面主要由“键盘”、“显示”及“标题栏”三个模块组成。其中,“键盘”与“显示”模块负责响应用户点击并控制运算表达式及运算结果的显示,实现了基础的计算功能。“菜单栏”模块为计算器顶部的菜单栏,是分布式计算功能的入口。

那么,如何实现分布式计算器各模块的功能?下面我们将从组件化、声明式描述和状态管理三个维度来解析分布式计算器的实现。


图1  计算器界面

1. 组件化

ArkUI开发框架定义了一些具有特殊含义的组件管理装饰器,如图2所示: 

图2 组件管理装饰器


根据声明式UI的组件化思想,我们可以将通过组件管理装饰器将计算器界面上的各个模块组件化为一个个独立的UI单元。 

2. 声明式描述

通过ArkUI开发框架提供的一系列基础组件,如Column、Text、Divider、Button等,以声明方式进行组合和扩展来对各个模块进行描述,包括参数构造配置、属性配置、事件配置以及子组件配置等,并通过基础的数据绑定和事件处理机制实现各个模块的逻辑交互。


3. 状态管理

ArkUI开发框架定义了一些具有特殊含义的状态管理装饰器,如图3所示:

图3 状态管理装饰器

通过状态管理装饰器装饰组件拥有的状态属性,当装饰的变量更改时,组件会重新渲染更新UI界面。

以上就是实现分布式计算器的核心原理,下面我们将为大家带来分布式计算器的基础计算功能与分布式功能的具体实现。

二、基础计算功能的实现

上文中提到,分布式计算器的基础计算功能由键盘模块及显示模块实现。

1. 键盘模块

键盘模块响应了用户的点击,并实现了计算器的基本功能。下面我们将介绍键盘布局以及键盘功能的实现。


(1) 键盘布局

计算器界面上的键盘,其实是一张张图片按照 4*5格式排列,如图4所示:

图4 键盘模块

首先,我们需要将所有图片保存至项目的media文件夹下,并初始化为ImageList,代码如下:

export function obtainImgVertical(): Array<Array>> {
  let list =
    [
      [
        { img: $r('app.media.ic_cal_seven'), value: '7' },
        { img: $r('app.media.ic_cal_eight'), value: '8' },
        { img: $r('app.media.ic_cal_nine'), value: '9' }
      ],
      [
        { img: $r('app.media.ic_cal_four'), value: '4' },
        { img: $r('app.media.ic_cal_five'), value: '5' },
        { img: $r('app.media.ic_cal_six'), value: '6' }
      ],
    ]
  return list
}
export function obtainImgV(): Array<ImageList> {
  let list =
    [
      { img: $r('app.media.ic_cal_delete'), value: '' },
      { img: $r('app.media.ic_cal_minus'), value: '-' },
      { img: $r('app.media.ic_cal_plus'), value: '+'  },
      { img: $r('app.media.ic_cal_equal'), value: '=' }
    ]
  return list
}

然后,我们需要对键盘模块进行组件化操作。这里我们通过@Component装饰器让键盘模块成为一个独立的组件。

最后,使用ArkUI开发框架提供的内置组件及属性方法进行声明性描述。这里我们使用了Grid组件进行布局,并通过ForEach组件来迭代图片数组实现循环渲染,同时还为每张图片添加了ClickButton事件方法。代码如下:

@Component
export struct ButtonComponent {
  private isLand: boolean
  private onInputValue: (result) => void
  build() {
    Row() {
      Grid() {
        ForEach(obtainImgV(), (item, index) => {
          GridItem() {
            Image(item.Img)
              .margin({ top: 5 })
              .onClick(() => {
                this.onInputValue(item.value)
              })
          }
          .rowStart(index)
          .rowEnd(index === 3 ? index + 1 : index)
          .columnStart(3)
          .columnEnd(3)
        })
        ForEach(obtainImgVertical(), (item) => {
          ForEach(item, (item) => {
            GridItem() {
              Image(item.Img)
                .margin({ top: 5 })
                .onClick(() => {
                  this.onInputValue(item.value)
                })
            }
          })
        })
      }
    }
  }
}


(2) 功能实现

按键功能包含了“归零”、“清除”、“计算”三个功能。


① 当用户点击“C”按钮后,运算表达式与运算结果“归零”,代码如下:

onInputValue = (value) => {
  if (value === 'C') { // 当用户点击C按钮,表达式和运算结果归0
    this.expression = ''
    this.result = ''
    return
  }
  // 输入数字,表达式直接拼接,计算运算结果
  this.expression += value
  this.result = JSON.stringify(MATH.evaluate(this.expression))
}

② 当用户点击“X”按钮后,删除运算表达式的最后一个字符。代码如下:

onInputValue = (value) => {
  if (value === '') { // 当用户点击删除按钮,表达式删除上一次的输入,重新运算表达式
    this.expression = this.expression.substr(0, this.expression.length - 1)
    this.result = JSON.stringify(MATH.evaluate(this.expression))
    return
  }
  if (this.isOperator(value)) { // 当用户输入的是运算符
    // 判断表达式最后一个字符是运算符则覆盖上一个运算符,否则表达式直接拼接
    if (this.isOperator(this.expression.substr(this.expression.length - 1, this.expression.length))) {
      this.expression = this.expression.substr(0, this.expression.length - 1)
      this.expression += value
    } else {
      this.expression += value
    }
    return
  }
  // 输入数字,表达式直接拼接,计算运算结果
  this.expression += value
  this.result = JSON.stringify(MATH.evaluate(this.expression))
}


③ 当用户点击“=”按钮后,将调用JavaScript的math.js库对表达式进行计算。代码如下:

import { create, all } from 'mathjs'
onInputValue = (value) => {
  if (value === '=') { // 当用户点击=按钮
    this.result = ''
    // 判断表达式最后一个字符是运算符,运算结果需要去掉最后一个运算符运算,否则直接运算
    if (this.isOperator(this.expression.substr(this.expression.length - 1, this.expression.length))) {
      this.expression = JSON.stringify(MATH.evaluate(this.expression.substr(0, this.expression.length - 1)))
    } else {
      this.expression = JSON.stringify(MATH.evaluate(this.expression))
    }
    return
  }
  // 输入数字,表达式直接拼接,计算运算结果
  this.expression += value
  this.result = JSON.stringify(MATH.evaluate(this.expression))
}


注:计算功能的实现依赖于JavaScript的math.js库,使用前需通过npm install mathjs--save命令下载并安装math.js库。

2. 显示模块

显示模块实现了“键入的运算表达式”与“运算结果”的显示,本质上是Text文本,如图5所示:

图5 显示模块

首先我们通过@Component装饰器使该模块具有组件化能力,然后在build方法里描述UI结构,最后使用@Link状态装饰器管理组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。代码如下: 

@Component
export struct InPutComponent {
  private isLand: boolean
  @Link result: string
  @Link expression: string
  build() {
    Stack({ alignContent: this.isLand ? Alignment.BottomStart : Alignment.TopEnd }) {
      Column() {
        //运算表达式文本框
        Scroll() {
          Text(this.expression)
            .maxLines(1)
            .opacity(0.9)
            .fontWeight(400)
            .textAlign(TextAlign.Start)
            .fontSize(this.isLand ? 50 : 35)
        }
        .width('90%')
        .scrollable(ScrollDirection.Horizontal)
        .align(this.isLand ? Alignment.Start : Alignment.End)
        //运算结果文本框
        Scroll() {
          Text(this.result)
            .maxLines(1)
            .opacity(0.38)
            .textAlign(TextAlign.Start)
            .fontSize(this.isLand ? 45 : 30)
            .margin(this.isLand ? { bottom: 64 } : {})
        }
      }
    }
  }
}


至此,一个初具计算功能的计算器就实现了。下面我们将实现计算器的分布式功能。

三、分布式功能的实现

计算器的分布式功能以菜单栏模块为入口,并基于分布式设备管理与分布式数据管理技术实现。

1. 菜单栏模块

“菜单栏”模块为计算器顶部菜单栏,是计算器分布式功能的入口。

图6 菜单栏模块

如图6所示,当用户点击图标 时,执行terminate()方法,退出计算器应用。当用户点击 按钮时,执行showDialog()方法,界面上弹出的分布式设备列表弹窗,选择设备后将获取分布式数据管理的权限,最后实现远端设备的拉起。代码如下:

@Component
export struct TitleBar {
  build() {
    Row() {
      Image($r("app.media.ic_back"))
        .height('60%')
        .margin({ left: 32 })
        .width(this.isLand ? '5%' : '8%')
        .objectFit(ImageFit.Contain)
        //执行terminate()方法,退出计算器应用
        .onClick(() => {
          app.terminate()
        })
      Blank().layoutWeight(1)
      Image($r("app.media.ic_hop"))
        .height('60%')
        .margin({ right: 32 })
        .width(this.isLand ? '5%' : '8%')
        .objectFit(ImageFit.Contain)
        //执行showDialog()方法,界面上弹出的分布式设备列表弹窗
        .onClick(() => {
          this.showDiainfo()
        })
    }
    .width('100%')
    .height(this.isLand ? '10%' : '8%')
    .constraintSize({ minHeight: 50 })
    .alignItems(VerticalAlign.Center)
  }
}


2. 分布式设备管理

在分布式计算器应用中,分布式设备管理包含了分布式设备搜索、分布式设备列表弹窗、远端设备拉起三部分。首先在分布式组网内搜索设备,然后把设备展示到分布式设备列表弹窗中,最后根据用户的选择拉起远端设备。


(1) 分布式设备搜索

通过SUBSCRIBE_ID搜索分布式组网内的远端设备,代码如下:

startDeviceDiscovery() {
  SUBSCRIBE_ID = Math.floor(65536 * Math.random())
  let info = {
    subscribeId: SUBSCRIBE_ID,
    mode: 0xAA,
    medium: 2,
    freq: 2,
    isSameAccount: false,
    isWakeRemote: true,
    capability: 0
  }
  Logger.info(TAG, `startDeviceDiscovery ${SUBSCRIBE_ID}`)
  this.deviceManager.startDeviceDiscovery(info)
}


(2) 分布式设备列表弹窗

分布式设备列表弹窗实现了远端设备的选择,如图7所示,用户可以根据设备名称选择相应的设备进行协同计算。

图7 分布式设备列表弹窗

这里我们使用@CustomDialog装饰器来装饰分布式设备列表弹窗,代码如下:

@CustomDialog
export struct DeviceDialog {
  build() {
    Column() {
      List() {
        ForEach(this.deviceList, (item, index) => {
          ListItem() {
            Row() {
              Text(item.deviceName)
                .fontSize(21)
                .width('90%')
                .fontColor(Color.Black)
              Image(index === this.selectedIndex ? $r('app.media.checked') : $r('app.media.uncheck'))
                .width('8%')
                .objectFit(ImageFit.Contain)
            }
            .height(55)
            .margin({ top: 17 })
            .onClick(() => {
                if (index === this.selectedIndex) {
                return
              }
              this.selectedIndex = index
              this.onSelectedIndexChange(this.selectedIndex)
            })
          }
        }, item => item.deviceName)
      }
    }
  }
}

(3) 远端设备拉起

通过startAbility(deviceId)方法拉起远端设备的FA,代码如下:

startAbility(deviceId) {
  featureAbility.startAbility({
    want: {
      bundleName: 'ohos.samples.DistributeCalc',
      abilityName: 'ohos.samples.DistributeCalc.MainAbility',
      deviceId: deviceId,
      parameters: {
        isFA: 'FA'
      }
    }
  }).then((data) => {
    this.startAbilityCallBack(DATA_CHANGE)
  })
}


3. 分布式数据管理

分布式数据管理用于实现协同计算时数据在多端设备之间的相互同步。我们需要创建一个分布式数据库来保存协同计算时数据,并通过分布式数据通信进行同步。


(1) 管理分布式数据库

创建一个KVManager对象实例,用于管理分布式数据库对象。代码如下:

async createKvStore(callback) {
  //创建一个KVManager对象实例 
  this.kvManager = await distributedData.createKVManager(config)
  let options = {
    createIfMissing: true,
    encrypt: false,
    backup: false,
    autoSync: true,
    kvStoreType: 1,
    securityLevel: 1,
  }
  // 通过指定Options和storeId,创建并获取KVStore数据库,并通过Promise方式返回,此方法为异步方法。
  this.kvStore = await this.kvManager.getKVStore(STORE_ID, options)
  callback()
}


(2) 订阅分布式数据变化

通过订阅分布式数据库所有(本地及远端)数据变化实现数据协同,代码如下:

kvStoreModel.setOnMessageReceivedListener(DATA_CHANGE, (value) => {
  if (this.isDistributed) {
    if (value.search(EXIT) != -1) {
      Logger.info(TAG, `EXIT ${EXIT}`)
      featureAbility.terminateSelf((error) => {
        Logger.error(TAG, `terminateSelf finished, error= ${error}`)
      });
    } else {
      this.expression = value
      if (this.isOperator(this.expression.substr(this.expression.length - 1, this.expression.length))) {
        this.result = JSON.stringify(MATH.evaluate(this.expression.substr(0, this.expression.length - 1)))
      } else {
        this.result = JSON.stringify(MATH.evaluate(this.expression))
      }
    }
  }
})


至此,具有分布式能力的计算器就实现了。期待广大开发者能基于TS扩展的声明式开发范式开发出更多有趣的应用。

点击链接,可获取分布式计算器完整代码:https://gitee.com/openharmony/app_samples/tree/master/Preset/DistributeCalc

搜索

复制