目录

Sendable Object Refactoring Practice

Introduction

This document describes the typical usage of @hadss/turbo-trans-json (hereinafter referred to as TurboTransJSON) and @hadss/turbo-trans-protobuf (hereinafter referred to as TurboTransProtobuf) in the TurboTrans third-party library for operating Sendable objects in ArkTS:

Using the TurboTrans Third-Party Library to Operate Sendable Objects

[h2]Use TurboTransJSON to Serialize/Deserialize and Generate Sendable Objects

  1. Environment configuration

    Import the TurboTrans third-party library.

      "dependencies": {
    // ...
        "@hadss/turbo-trans-json-plugin": "latest",
      },

    Install the third-party library in the project directory or module using ohpm.

    ohpm install @hadss/turbo-trans-core @hadss/turbo-trans-json

    For the plugin to take effect, add the relevant plugin configuration to the hvigorfile.ts file in the project root directory.

    import { turboTransJsonPlugin } from '@hadss/turbo-trans-json-plugin';
    import { hvigor } from '@ohos/hvigor';
    import { appTasks } from '@ohos/hvigor-ohos-plugin';
    
    export default {
      system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
      plugins: [
        turboTransJsonPlugin(hvigor, {
          ignoreModuleNames: ['TurboTransCore', 'TurboTransJSON', 'PerformanceBaseline', 'TurboTransProtobuf'], // Ignored modules
          scanDir: ['src/main/ets'], // Scan directory
          deserializationMode: 'performance', // Deserialization mode
        }),
      ]       /* Custom plugin to extend the functionality of Hvigor. */
    }
  2. Define a serializable model.

    Mark the class to be serialized with the @Serializable decorator, add the generateSendable property, and use @SerialName() to customize the property configuration, where:

    • @Serializable({ generateSendable: true }): indicates that the Sendable type and conversion method corresponding to this model need to be generated.

    • @SerialName({ name: 'xxx' }): binds class properties to JSON field names.

      // Path of this file in the project: entry/src/main/ets/turbotrans_JSON/layout.ets
      import { Serializable, SerialName } from '@hadss/turbo-trans-core';
      import { collections } from '@kit.ArkTS';
      // ...
      
      @Serializable({ generateSendable: true})
      export class Layout {
        @SerialName({ name: 'type'})
        public type: string = '';
        @SerialName({ name: 'arr'})
        public arr: number[] = [];
      }
      
      @Sendable
      export class LayoutS {
        public type: string = '';
        public arr: collections.Array<number> = new collections.Array();
      }
  3. Deserialize from a JSON string to a plain object (TJSON.fromString).

    Use TJSON.fromString<Model>(jsonStr, Model) to deserialize from a JSON string to a plain object. You can use ArkTSUtils.isSendable(obj) for verification. The project example demonstrates two input sources, and after deserialization it continues to convert the plain object to a Sendable object, forming the first half of the "JSON string -> plain object -> Sendable object" chain.

    • The testJSON1 method serializes a plain object to a JSON string through JSON.stringify(), then deserializes the JSON string back to a plain object, and finally converts it to a Sendable object.

    • The testJSON2 method deserializes a hand-written JSON string to a plain object, and then converts it to a Sendable object.

      // Path of this file in the project: entry/src/main/ets/turbotrans_JSON/test1.ets
      import { TJSON } from '@hadss/turbo-trans-json';
      import { Layout, LayoutS } from 'entry/ets/turbotrans_JSON/layout';
      import type { ITSerializable } from '@hadss/turbo-trans-json';
      import { ArkTSUtils } from '@kit.ArkTS';
      
      export function testJSON1(): LayoutS {
        let obj = new Layout();
        obj.type = 'Test';
        obj.arr = [3, 4];
        let str = JSON.stringify(obj);
        let layoutNormal = TJSON.fromString<Layout>(str, Layout);
        console.info('testJSON1 layout arr: ' + layoutNormal.arr);
        let layoutSendable = (layoutNormal as object as ITSerializable).toSendable();
        if (ArkTSUtils.isSendable(layoutSendable)) {
          console.info('expect layout from JSON string is Sendable');
        } else {
          console.error('test occur error');
        }
        let layoutS = layoutSendable as LayoutS;
        return layoutS;
      }
      
      export function testJSON2(): LayoutS {
        let layoutStr = '{"type":"Text","arr":[3,4]}';
        let layoutNormal = TJSON.fromString<Layout>(layoutStr, Layout);
        console.info('testJSON2 layout arr: ' + layoutNormal.arr);
        let layoutSendable = (layoutNormal as object as ITSerializable).toSendable();
        if (ArkTSUtils.isSendable(layoutSendable)) {
          console.info('expect layout from simple string is Sendable');
        } else {
          console.error('test occur error');
        }
        let layoutS = layoutSendable as LayoutS;
        return layoutS;
      }
  4. Convert a plain object to a Sendable object (toSendable).

    When generateSendable: true takes effect, toSendable() is generated in the build output to convert a plain object to a Sendable object. The key point is that it performs necessary container conversion on the plain object, for example converting number[] to collections.Array<number>. The following code is automatically generated by the compile-time tool, and the file is located in the entry/src/generated/ets/turbotrans_JSON directory.

    import { collections } from '@kit.ArkTS';
    
    toSendable(): SendableLayout {
      const sendable = new SendableLayout();
    
      // Assign a non-constructor property.
      sendable.type = this.type;
      sendable.arr = new collections.Array(...this.arr);
    
      return sendable;
    }

Meanwhile, the generated Sendable type usually provides toOrigin(), which is used to restore a Sendable object to a plain object when you need to continue processing it as a plain object, reuse existing logic, or serialize it again. The following code is automatically generated by the compile-time tool and is located in the entry/src/generated/ets/turbotrans_JSON directory.

@Sendable
export class SendableLayout implements lang.ISendable {
  public type: string = '';
  public arr: collections.Array<number> = new collections.Array();

  toOrigin(): Layout {
    const origin = new Layout();
    origin.type = this.type;
    origin.arr = new Array(...this.arr);
    return origin;
  }
}

The JSON path forms a complete loop:

JSON string -> TJSON.fromString() -> plain object -> toSendable() -> Sendable object -> toOrigin() -> plain object.

[h2]Use TurboTransProtobuf to Generate Sendable Objects and Encode/Decode

  1. Environment configuration

    Import the TurboTrans third-party library.

    "dependencies": {
        // ...
        "@hadss/turbo-trans-protobuf-plugin": "latest"
    },

    Install the third-party library using ohpm in the project directory or module.

    ohpm install @hadss/turbo-trans-protobuf

    For the plugin to take effect, add the relevant plugin configuration to the hvigorfile.ts file in the project root directory.

    import { turboTransProtobufPlugin } from '@hadss/turbo-trans-protobuf-plugin';
    import { hvigor } from '@ohos/hvigor';
    import { appTasks } from '@ohos/hvigor-ohos-plugin';
    
    export default {
      system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
      plugins: [
        turboTransProtobufPlugin({
          saveDir: 'src/main/ets/protobuf', // Directory for saving generated code.
          ignoreModuleNames: ['TurboTransCore', 'PerformanceBaseline', 'TurboTransProtobufCommon'], // Modules to ignore.
          sendable: true, // Whether to enable sendable.
          debug: false, // Whether to enable debug.
          scanDir: ['protobuf'], // Directories to scan.
        }),
      ]       /* Custom plugin to extend the functionality of Hvigor. */
    }
  2. Write the proto file.

    The .proto file must be placed in the scanDir directory configured for the turboTransProtobufPlugin in the hvigorfile.ts file at the project root directory to take effect. For example, in the sample project, the .proto file is located at entry/protobuf/test_pb.proto.

    syntax = "proto3";
    
    message test_pb {
        int32 value_int32 = 1;
        int64 value_int64 = 2;
        uint32 value_uint32 = 3;
        uint64 value_uint64 = 4;
        sint32 value_sint32 = 5;
        sint64 value_sint64 = 6;
        fixed32 value_fixed32 = 7;
        fixed64 value_fixed64 = 8;
        sfixed32 value_sfixed32 = 9;
        sfixed64 value_sfixed64 = 10;
        float value_float = 11;
        double value_double = 12;
        bool value_bool = 13;
        string value_string = 14;
        bytes value_bytes = 15;
    }
  3. Build to generate the Sendable message class.

    After building, the Sendable message class is generated in the saveDir result directory configured for the turboTransProtobufPlugin in the hvigorfile.ts file at the project root directory. For example, in the sample project, entry/src/main/ets/protobuf/test_pb.ets is generated.

    • test_pb.create(): Creates an instance.

    • test_pb.encode(message): Encodes the instance into an ArrayBuffer.

    • test_pb.decode(buffer): Decodes the binary data back to test_pb.

  4. Create a Sendable message object and perform encoding and decoding.

    Use the encode/decode methods provided by the generated class to complete binary serialization/deserialization, and the message object obtained after decoding can still be passed onward as a Sendable object. After encoding and decoding, test_pb still retains the Sendable feature, so it can also continue to enter the UI binding process as the return value of a concurrent task, just like the Sendable object generated through the JSON path. In the following example, the testProtobuf method first creates a test_pb instance, then encodes it into binary data, and finally decodes it back to test_pb and verifies that it is still a Sendable object.

    import { test_pb } from '../protobuf/test_pb'
    import { ArkTSUtils, collections } from '@kit.ArkTS';
    
    function testCreate() {
      const obj = test_pb.create();
      obj.value_int32 = 1;
      obj.value_int64 = 2;
      obj.value_uint32 = 3;
      obj.value_uint64 = 4;
      obj.value_sint32 = 5;
      obj.value_sint64 = 6;
      obj.value_fixed32 = 7;
      obj.value_fixed64 = 8;
      obj.value_sfixed32 = 9;
      obj.value_sfixed64 = 10;
      obj.value_float = 11;
      obj.value_double = 12;
      obj.value_bool = true;
      obj.value_string = `TestProtobuf_Success`;
      obj.value_bytes?.fill(13);
    
      if (ArkTSUtils.isSendable(obj)) {
        console.info("create a sendable object");
      }
    
      return obj;
    }
    
    function testencode(obj: test_pb) {
      return test_pb.encode(obj);
    }
    
    function testdecode(data: ArrayBuffer | collections.ArrayBuffer) {
      let obj = test_pb.decode(data);
      console.info(`expect value_int32 ${obj?.value_int32} = 1`);
      console.info(`expect value_double ${obj?.value_double} = 12`);
      if (ArkTSUtils.isSendable(obj)) {
        console.info("decode a sendable object");
      }
    }
    
    export function testProtobuf(): test_pb {
      let obj = testCreate();
      let buf = testencode(obj);
      if (buf) {
        testdecode(buf);
      }
      return obj;
    }

Use makeObserved to Convert a Sendable Object to an Observable Object

Use the UIUtils.makeObserved() method to convert a Sendable object to an observable object.

[h2]Define Concurrent Tasks

Define the observeJSON1 and observeJSON2 concurrent tasks

import { LayoutS } from '../turbotrans_JSON/layout';
import { testJSON1, testJSON2 } from '../turbotrans_JSON/test1';

@Concurrent
export function observeJSON1(): LayoutS {
  return testJSON1();
}

@Concurrent
export function observeJSON2(): LayoutS {
  return testJSON2();
}

Define the observeProtobuf concurrent task

import { testProtobuf } from '../turbotrans_protobuf/test1'
import { test_pb } from '../protobuf/test_pb'

@Concurrent
export function observeProtobuf(): test_pb {
  return testProtobuf();
}

[h2]Execute an Asynchronous Task Through taskpool and Convert a Sendable Object to an Observable Object

In runTests(), a concurrent task is executed through taskpool.execute(). After the returned Sendable object is obtained, it is wrapped as an observable object with UIUtils.makeObserved() and written back to the UI state. In this way, the data link becomes "concurrent task return -> convert to observable object -> component auto refresh".

The object engineering example of executing the observeJSON1 concurrent task and returning a Sendable object is as follows:

runTests() {
  taskpool.execute(observeJSON1).then((res) => {
    this.layout = UIUtils.makeObserved(res as LayoutS);
  })
}

The object engineering example of executing the observeProtobuf concurrent task and returning a Sendable object is as follows:

runTestsPb() {
  taskpool.execute(observeProtobuf).then((res) => {
    this.pb = UIUtils.makeObserved(res as test_pb);
  })
}

[h2]Create a UI component to observe Sendable object property changes in real time

In Index.ets, the task return value is saved through the @Local state, and then the Text component binds pb.value_string and layout.type. Here, pb stores the Sendable object (of the test_pb type) returned by observeProtobuf, and layout stores the Sendable object (of the LayoutS type) returned by observeJSON1.

The Sendable object engineering example returned by observeJSON1 is as follows:

import { LayoutS } from '../turbotrans_JSON/layout';
import { taskpool } from '@kit.ArkTS';
import { observeJSON1 } from './concurrentFunc';
import { UIUtils } from '@kit.ArkUI';

@Entry
@ComponentV2
struct Index {
  @Local message: string = 'Hello World';
  // Replace with pb: test_pb = test_pb.create(); to use the scenario where observeProtobuf returns a Sendable object.
  // @Local test_pb = test_pb.create();
  @Local layout: LayoutS = new LayoutS();

  build() {
    Column() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })

      Button('Run TaskPool test')
        .width('80%')
        .height(50)
        .margin({ top: 20, bottom: 20 })
        .onClick(() => {
          this.runTests();
        })
        .id('button')

      Scroll() {
        Column() {

          Text(`TestJSON: ${this.layout.type}`)
            .fontSize(24)
            .textAlign(TextAlign.Start)
            .backgroundColor(Color.White)
            .padding(10)
            .borderRadius(8)
            .margin({ bottom: 10 })
            .width('100%')
        }
        .width('100%')
        .padding(10)
      }
      .height('60%')
      .width('100%')
    }
    .height('100%')
    .width('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }

// ...
}