OpenHarmony开发者论坛

标题: Flutter 到 OpenHarmony,不是有手就行吗? (下拉刷新) [打印本页]

作者: zmtzawqlp    时间: 2023-12-19 09:45
标题: Flutter 到 OpenHarmony,不是有手就行吗? (下拉刷新)
[md]* [Flutter 下拉刷新花式玩法 - 掘金 (juejin.cn)](https://juejin.cn/post/6844903714117910541)

## 前言

五年前,有人告诉我,你可以错过其他技术,但千万不要错过 `Flutter` 。然而此刻,有人告诉我,如果你错过了 OpenHarmony,恐怕要错过下个时代了。

作为发展了 ` 5` 年的 [FlutterCandies](https://github.com/fluttercandies) 社区,我们已拥有 `70+` 的 `Flutter` 组件。我们当然也不会止步于 `Flutter` 。我们希望把我们的 `Flutter` 组件也能带到 OpenHarmony 生态当中,[HarmonyCandies](https://github.com/HarmonyCandies) 便是为了这一刻。

以 `Flutter` 开发者的角度,尽可能提供相同 `Api` 的 OpenHarmony 组件。

本文默认您已经有一定的 OpenHarmony 开发经验,并且阅读过以下内容。

* [组件导读-组件参考(基于ArkTS的声明式开发范式)-ArkTS API参考-HarmonyOS应用开发](https://developer.harmonyos.com/ ... 3?catalogVersion=V3)
* [基本语法概述-快速入门-入门-HarmonyOS应用开发](https://developer.harmonyos.com/ ... 0000001531611153-V3)
* [开发说明-ArkTS接口参考-ArkTS API参考-HarmonyOS应用开发](https://developer.harmonyos.com/ ... 0000001478061813-V3)

使用的 `ide` 版本为 ` DevEco Studio 4.0 Release` [OpenHarmony v4.0 Release (2023-10-26)](http://docs.openharmony.cn/pages ... ony-v4.0-release.md) ,开发 `sdk` 为 `api 9`,当然也适配了 `api 10` 。

![DM_20231219092555_001.jpg](https://forums-obs.openharmony.c ... moomormwrvgwora.jpg "DM_20231219092555_001.jpg")

## 下拉刷新

列表在一个 `App` 中最常见的呈现方式,而下拉刷新是其常见的一种效果。

在 `Flutter` 中你可以通过
[pull_to_refresh_notification](https://github.com/fluttercandies/pull_to_refresh_notification) 来实现一个可以自定义任何效果的下拉刷新。

在.OpenHarmony 中你则可以使用  https://github.com/HarmonyCandies/pull_to_refresh 来实现。

| ![PullToRefreshAppbar.gif](https://forums-obs.openharmony.c ... 0f0y2oe5peo5xzx.gif "PullToRefreshAppbar.gif") | ![PullToRefreshHeader.gif](https://forums-obs.openharmony.c ... 5nnn0iz3olstfos.gif "PullToRefreshHeader.gif") |
| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |

## 安装

你可以通过下面的命令来下载安装

`ohpm install @candies/pull_to_refresh`

## 参数

### PullToRefreshIndicatorMode

```typescript
export enum PullToRefreshIndicatorMode {
  initial, // 初始状态
  drag, // 手势向下拉的状态.
  armed, // 被拖动得足够远,以至于触发“onRefresh”回调函数的上滑事件
  snap, // 用户没有拖动到足够远的地方并且释放回到初始化状态的过程
  refresh, // 正在执行刷新回调.
  done, // 刷新回调完成.
  canceled, // 用户取消了下拉刷新手势.
  error, // 刷新失败
}
```

### 配置参数

| 参数                    | 类型            | 描述                                                 |
| ----------------------- | --------------- | ---------------------------------------------------- |
| maxDragOffset           | number          | 最大拖动距离(非必填)                                 |
| reachToRefreshOffset    | number          | 到达满足触发刷新的距离(非必填)                       |
| refreshOffset           | number          | 触发刷新的时候,停留的刷新距离(非必填)               |
| pullBackOnRefresh       | boolean         | 在触发刷新回调的时候是否执行回退动画(默认 `false`) |
| pullBackAnimatorOptions | AnimatorOptions | 回退动画的一些配置(duration,easing,delay,fill)     |
| pullBackOnError         | boolean         | 刷新失败的时候,是否执行回退动画(默认 `false`)     |

* `maxDragOffset` 和 `reachToRefreshOffset` 如果不定义的话,会根据当前容器的高度设置默认值。

```
/// Set the default value of [maxDragOffset,reachToRefreshOffset]
onAreaChange(oldValue: Area, newValue: Area) {
  if (this.maxDragOffset == undefined) {
    this.maxDragOffset = (newValue.height as number) / 5;
  }
  if (this.reachToRefreshOffset == undefined) {
    this.reachToRefreshOffset = this.maxDragOffset * 3 / 4;
  }
  else {
    this.reachToRefreshOffset = Math.min(this.reachToRefreshOffset, this.maxDragOffset);
  }
}
```

* `pullBackAnimatorOptions` 的默认值如下:

```typescript
/// The options of pull back animation
pullBackAnimatorOptions: AnimatorOptions = {
  duration: 400,
  easing: "friction",
  delay: 0,
  fill: "forwards",
  direction: "normal",
  iterations: 1,
  begin: 1.0,
  end: 0.0
};
```

### 回调

#### onRefresh

触发的下拉刷新事件

```typescript
/// A function that's called when the user has dragged the refresh indicator
/// far enough to demonstrate that they want the app to refresh. The returned
/// [Future] must complete when the refresh operation is finished.

onRefresh: RefreshCallback = async () => true;
```

#### onReachEdge

是否我们到达了下拉刷新的边界,比如说,下拉刷新的内容是一个列表,那么边界就是到达列表的顶部位置。

```typescript
/// Whether we reach the edge to pull refresh
onReachEdge: () => boolean = () => true;
```

## 使用

### 导入引用

```typescript
import {
  PullToRefresh,
  pull_to_refresh,
  PullToRefreshIndicatorMode,
} from '@candies/pull_to_refresh'
```

### 定义配置

```typescript
@State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller();
```

### 使用 PullToRefresh

将需要支持下拉刷新的部分,通过 `@BuilderParam` 修饰的 `builder` 回调传入,或者尾随闭包初始化组件。

[@BuilderParam装饰器:引用@Builder函数-快速入门-入门-HarmonyOS应用开发](https://developer.harmonyos.com/ ... 0000001524416541-V3)

```typescript
PullToRefresh(
    {
      refreshOffset: 150,
      maxDragOffset: 300,
      reachToRefreshOffset: 200,  
      controller: this.controller,
      onRefresh: async () => {
        return new Promise<boolean>((resolve) => {
          setTimeout(() => {
            // 定义的刷新方法,当刷新成功之后,返回回调,模拟 2 秒之后刷新完毕
            this.onRefresh().then((value) => resolve(value));
          }, 2000);
        });
      },
      onReachEdge: () => {
        let yOffset = this.scroller.currentOffset().yOffset;
        return Math.abs(yOffset) < 0.001;
      }
    }) {
    // 我们自定义的下拉刷新头部
    PullToRefreshContainer({
      lastRefreshTime: this.lastRefreshTime,
      controller: this.controller,
    })
    List({ scroller: this.scroller }) {
      ForEach(this.listData, (item, index) => {
        ListItem() {
          Text(`${item}`,).align(Alignment.Center)
        }.height(100).width('100%')
      }, (item, index) => {
        return `${item}`;
      })
    }
    // 必须设置 edgeEffect
    .edgeEffect(EdgeEffect.None)
    // 为了使下拉刷新的手势的过程中,不触发列表的滚动
    .onScrollFrameBegin((offset, state) => {
      if (this.controller.dragOffset > 0) {
        offset = 0;
      }
      return { offsetRemain: offset, };
    })
  }
}
```

### 自定义下拉刷新效果

你可以通过对 `Controller` 中 `dragOffset` 和 `mode` 的判断,创建属于自己的下拉刷新效果。如果下拉刷新失败了,你可以通过调用 `Controller` 的 `refresh() `方法来重新执行刷新动画。

```typescript
/// The current drag offset
dragOffset: number = 0;
/// The current pull mode
mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial;
```

下面是一个自定义下拉刷新头部的例子

```typescript
@Component
struct PullToRefreshContainer {
  @Prop lastRefreshTime: number = 0;
  @Link controller: pull_to_refresh.Controller;

  getShowText(): string {
    let text = '';
    if (this.controller.mode == PullToRefreshIndicatorMode.armed) {
      text = 'Release to refresh';
    } else if (this.controller.mode == PullToRefreshIndicatorMode.refresh ||
      this.controller.mode == PullToRefreshIndicatorMode.snap) {
      text = 'Loading...';
    } else if (this.controller.mode == PullToRefreshIndicatorMode.done) {
      text = 'Refresh completed.';
    } else if (this.controller.mode == PullToRefreshIndicatorMode.drag) {
      text = 'Pull to refresh';
    } else if (this.controller.mode == PullToRefreshIndicatorMode.canceled) {
      text = 'Cancel refresh';
    } else if (this.controller.mode == PullToRefreshIndicatorMode.error) {
      text = 'Refresh failed';
    }
    return text;
  }

  getDate(): String {
    return (new Date(this.lastRefreshTime)).toTimeString();
  }

  build() {
    Row() {
      if (this.controller.dragOffset != 0)
        Text(`${this.getShowText()}---${this.getDate()}`)
      if (this.controller.dragOffset > 50 && this.controller.mode == PullToRefreshIndicatorMode.refresh)
        LoadingProgress().width(50).height(50)
    }
    .justifyContent(FlexAlign.Center)
    .height(this.controller.dragOffset)
    .width('100%')
    .onClick(() => {
      if (this.controller.mode == PullToRefreshIndicatorMode.error) {
        this.controller.refresh();
      }
    })
    .backgroundColor('#22808080')
  }
}
```

## 学废了

虽然练习时长只有一个月,但通过编写第一个 `ArtUI` 组件,还是学到了不少东西。

### 创建发布一个组件

#### 创建组织

先到 [OpenHarmony三方库中心仓](https://ohpm.openharmony.cn/#/cn/home) 上面注册个账号,到 `个人中心` =》`组织管理` 中,申请一个组织。这个组织名字以后要用到,因为普通三方作者,是不能使用 `ohos` 前缀的。

比如我注册的是组织名为 `candies`,组件为 `pull_to_refresh`。那么组件最终的名字就是 `@candies/pull_to_refresh`

最后用户可以通过 `ohpm install @candies/pull_to_refresh`,来安装使用组件。

> 为啥这个要先做,因为审核很慢。

#### 创建项目

写一个组件,必然也会给这个组件创建一个演示例子,在 `Flutter` 中发布一个组件,你可以使用下面的结构。

```
package
--example
```

而在 `OpenHarmony` 里面你只能使用下面的结构,这样才能方便你修改代码。

```
example
--package
```

`2` 种结构的区别是, `package` 下面肯定会需要加 `README`,`LICENSE`,但是 `github`,`gitee` 默认只会显示根目录下面的 `README`,第二种结构就要多复制一份到 `example` 目录下面。

但是  [OpenHarmony三方库中心仓](https://ohpm.openharmony.cn/#/cn/home) 却要求,有点难顶啊。

![DM_20231219092555_004.jpg](https://forums-obs.openharmony.c ... e1c8n0mrmikq82l.jpg "DM_20231219092555_004.jpg")

> `ide` 啥时候支持下第一种结构呀!

##### 创建组件演示项目

创建一个项目。

![DM_20231219092555_005.jpg](https://forums-obs.openharmony.c ... 1r1tykzpf71m0k0.jpg "DM_20231219092555_005.jpg")

##### 创建组件项目

创建一个 `Static Libray` (至于其他 `Module` 是什么意思,请自行查看文档)
![DM_20231219092555_006.jpg](https://forums-obs.openharmony.c ... 7zmcbbm5c5uumkz.jpg "DM_20231219092555_006.jpg")

创建好的目录长这样子

![DM_20231219092555_007.jpg](https://forums-obs.openharmony.c ... rso6r6ocvdbo4fz.jpg "DM_20231219092555_007.jpg")

`oh-package.json5` 中是你的组件的信息。

这里你需要把名字改成 `@candies/pull_to_refresh` 即 `(@你的组织名字/组件名字)`

其他字段,请查看 [OpenHarmony三方库中心仓](https://ohpm.openharmony.cn/#/cn/help/createfile)

一个完整的 `oh-package.json5` 是这样的

```json
{
  "license": "Apache-2.0",
  "devDependencies": {},
  "keywords": [
    "pull",
    "refresh",
    "pulltorefresh"
  ],
  "author": "zmtzawqlp",
  "name": "@candies/pull_to_refresh",
  "description": "Harmony plugin for building pull to refresh effects with PullToRefresh quickly.",
  "main": "index.ets",
  "repository": "https://github.com/HarmonyCandies/pull_to_refresh",
  "version": "1.0.0",
  "homepage": "https://github.com/HarmonyCandies/pull_to_refresh",
  "dependencies": {}
}
```

组件项目中 `Index.ets` 是入口,用于导出组件。跟 `Flutter` 中 `lib` 下面带 `library 组件名;` 标识的 `dart` 文件效果一样。

```typescript
export { MainPage } from './src/main/ets/components/mainpage/MainPage'
```

##### 引用组件项目

要想 `Example` 能引用到 `pull_to_refresh`, 你还需要到

`entry\oh-package.json5` 中添加引用,这样你就可以编写组件的示例了。

```json
{
  "license": "",
  "devDependencies": {},
  "author": "",
  "name": "entry",
  "description": "Please describe the basic information.",
  "main": "",
  "version": "1.0.0",
  "dependencies": {
    "@candies/pull_to_refresh": "file:../pull_to_refresh"
  }
}
```

#### 发布

在准备发布之前,请先阅读 [贡献三方库](https://ohpm.openharmony.cn/#/cn/help/createfile) 下面内容。

![DM_20231219092555_008.jpg](https://forums-obs.openharmony.c ... rrb33p35rkk2fpb.jpg "DM_20231219092555_008.jpg")

阅读操作完毕之后,你就可以打你的 `har` 包了。选中你的组件项目,在 `Build` 下面选择 `Make Module 你的组件名字`。编译完成之后,你就可以在组件项目路径 `build\default\outputs\default\` 中找到你即将发布的包。

![DM_20231219092555_009.jpg](https://forums-obs.openharmony.c ... 111km8qxv10l589.jpg "DM_20231219092555_009.jpg")

最后执行 `ohpm publish xxx.har`(`xxx.har` 为上传包的本地路径)。上传成功之后,你就可以看到你的个人中心里面的消息和状态了,耐心等待审核。

我遇到的上架的问题主要是组织名称(当然,这是我自己猜的,后面会聊到这个),`ohos` 不是普通三方开发者使用的前缀, `ohos` 的库都在 [OpenHarmony-TPC: OpenHarmony third party components (gitee.com)](https://gitee.com/openharmony-tpc)下面。按道理你可以 `pr` 到这个下面,并且加入到 `ohos` 中,再发布。当然更欢迎大家能加入 `candies` 组织,大家一起生产有趣的小组件。

![DM_20231219092555_010.jpg](https://forums-obs.openharmony.c ... vhh87jnvhjnwn1r.jpg "DM_20231219092555_010.jpg")

![DM_20231219092555_011.jpg](https://forums-obs.openharmony.c ... 71zn9i6bn3dbx9l.jpg "DM_20231219092555_011.jpg")

![DM_20231219092555_012.jpg](https://forums-obs.openharmony.c ... 4rlk9jxokvjlooo.jpg "DM_20231219092555_012.jpg")

### @Provide/@Consume

第一眼看到这个状态管理装饰器的时候,好亲切的感觉。这不是就是 `Flutter` 里面的 ([provider | Flutter Package (flutter-io.cn)](https://pub-web.flutter-io.cn/packages/provider)) 吗?

最开始设计 [pull_to_refresh](https://github.com/HarmonyCandies/pull_to_refresh) 的时候,想着跟 `Flutter` 中一样,父组件里面存放管理下拉刷新的状态,然后子组件里面监听状态,达到局部刷新的效果。

第一版的设计结构如下:

* `CustomWidget` 中提供了 ` @Provide('a')`
* `CustomWidgetChild` 中使用 `@Consume('a')` 获取状态变化。

```typescript
@Entry
@Component
struct HomePage {

  @Builder
  builder2($$: { a: string }) {
    Text(`${$$.a}测试`)
  }


  build() {
    Column() {
      CustomWidget() {
        CustomWidgetChild({ builder: this.builder2 })
      }
    }
  }
}


@Component
struct CustomWidget {
  @Provide('a') a: string = 'abc';
  @BuilderParam
  builder: () => void;

  build() {
    Column() {
      Button('你好').onClick((x) => {
        if (this.a == 'ddd') {
          this.a = 'abc';
        }
        else {
          this.a = 'ddd';
        }

      })
      this.builder()
    }
  }
}


@Component
struct CustomWidgetChild {
  @Consume('a') a: string;
  @BuilderParam
  builder: ($$: { a: string }) => void;

  build() {
    Column() {
      this.builder({ a: this.a })
    }
  }
}
```

运行会报找不到 `Provide` 的错误。

通过分析由 `ArkTS` 生成的 `js` 文件(生成的 `js` 在 `entry\build\default\cache\default\default@CompileArkTS\esmodule\debug` 路径下面) ,我们可以分析得出:
`CustomWidgetChild` 其父组件实际上是 `HomePage`,其内部 `this` 指向的也是 `HomePage`,因此找不到 `CustomWidget` 的 `@Provide` 变量。

```js
class HomePage extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    builder2($$, parent = null) {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create(`${$$.a}测试`);
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Column.create();
            if (!isInitialRender) {
                Column.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        {
            this.observeComponentCreation((elmtId, isInitialRender) => {
                ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                if (isInitialRender) {
                    ViewPU.create(new CustomWidget(this, {
                        builder: () => {
                            {
                                this.observeComponentCreation((elmtId, isInitialRender) => {
                                    ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                                    if (isInitialRender) {
                                        ViewPU.create(new CustomWidgetChild(this, { builder: this.builder2 }, undefined, elmtId));
                                    }
                                    else {
                                        this.updateStateVarsOfChildByElmtId(elmtId, {});
                                    }
                                    ViewStackProcessor.StopGetAccessRecording();
                                });
                            }
                        }
                    }, undefined, elmtId));
                }
                else {
                    this.updateStateVarsOfChildByElmtId(elmtId, {});
                }
                ViewStackProcessor.StopGetAccessRecording();
            });
        }
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}
class CustomWidget extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.__a = new ObservedPropertySimplePU('abc', this, "a");
        this.addProvidedVar("a", this.__a);
        this.builder = undefined;
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
        if (params.a !== undefined) {
            this.a = params.a;
        }
        if (params.builder !== undefined) {
            this.builder = params.builder;
        }
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        this.__a.aboutToBeDeleted();
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    get a() {
        return this.__a.get();
    }
    set a(newValue) {
        this.__a.set(newValue);
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Column.create();
            if (!isInitialRender) {
                Column.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Button.createWithLabel('你好');
            Button.onClick((x) => {
                if (this.a == 'ddd') {
                    this.a = 'abc';
                }
                else {
                    this.a = 'ddd';
                }
            });
            if (!isInitialRender) {
                Button.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Button.pop();
        this.builder.bind(this)();
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}
class CustomWidgetChild extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.__a = this.initializeConsume("a", "a");
        this.builder = undefined;
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
        if (params.builder !== undefined) {
            this.builder = params.builder;
        }
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        this.__a.aboutToBeDeleted();
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    get a() {
        return this.__a.get();
    }
    set a(newValue) {
        this.__a.set(newValue);
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Column.create();
            if (!isInitialRender) {
                Column.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        this.builder.bind(this)(makeBuilderParameterProxy("builder", { a: () => (this["__a"] ? this["__a"] : this["a"]) }));
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}
ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent());
loadDocument(new HomePage(undefined, {}));
ViewStackProcessor.StopGetAccessRecording();
export {};
//# sourceMappingURL=Index.js.map
```

意思就是你只能写成下面的这种形式。虽然说 `CustomWidgetChild` 是看起来是通过 `CustomWidget` 的 `builder` 创建出来的,但是它们依然没有父子关系,这跟 `Flutter` 完全不是一套原理。

```typescript
@Entry
@Component
struct HomePage {
  @Provide('a') test: string = 'abc';

  @Builder
  builder2($$: { a: string }) {
    Text(`${$$.a}测试`)
  }

  build() {
    Column() {
      CustomWidget() {
        CustomWidgetChild({ builder: this.builder2 })
      }
    }
  }
}


@Component
struct CustomWidget {

  @Consume('a') a: string;
  @BuilderParam
  builder: () => void;

  build() {
    Column() {
      Button('你好').onClick((x) => {
        if (this.a == 'ddd') {
          this.a = 'abc';
        }
        else {
          this.a = 'ddd';
        }
      })
      this.builder()
    }
  }
}


@Component
struct CustomWidgetChild {
  @Consume('a') a: string;
  @BuilderParam
  builder: ($$: { a: string }) => void;

  build() {
    Column() {
      this.builder({ a: this.a })
    }
  }
}
```

### @Builder/@BuilderParam

在自定义组件中,如果你想传入其他的组件,你需要使用到 `@Builder`和 `@BuilderParam`, 代码如下:

```typescript
@Component
struct Child {
  @BuilderParam aBuilder0: () => void;

  build() {
    Column() {
      this.aBuilder0()
    }
  }
}

@Entry
@Component
struct Parent {
  @Builder componentBuilder() {
    Text(`Parent builder `)
  }

  build() {
    Column() {
      Child({ aBuilder0: this.componentBuilder })
    }
  }
}
```

但是实际中写一个自定义组件的时候,会有这种需求。需要为 `BuilderParam` 修饰的内容的返回增加一些事件或者设置。比如下面例子,为 `BuilderParamChild` 的 `builder` 的返回增加 `hitTestBehavior` 设置。我这里将 `builder` 的返回修改为了 `CommonMethod<any>`(组件都继承于该类,里面是一些公共的属性,事件),虽然这样可以让编辑器有提示,并且不报错,但是运行起来依然会提示 `hitTestBehavior` 找不到。

```typescript
@Component
struct BuilderParamTestDemo {
  build() {
    Column(){
      BuilderParamChild(){
        Text('测试')
      }
    }
  }
}


@Component
struct BuilderParamChild {
  @BuilderParam
  builder: () => CommonMethod<any>;

  build() {
    this.builder().hitTestBehavior(HitTestMode.None)
  }
}
```

错误堆栈如下:

```
E  Error message: Cannot read property hitTestBehavior of undefined
E  SourceCode:
E          this.builder().hitTestBehavior.bind(this)(HitTestMode.None);
E          ^
E  Stacktrace:
E      at initialRender (entry/src/main/ets/pages/Index.ets:20:5)
```

从生成的 `js` 中也能看到对应的代码。

```js
"use strict";
class BuilderParamTestDemo extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Column.create();
            if (!isInitialRender) {
                Column.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        {
            this.observeComponentCreation((elmtId, isInitialRender) => {
                ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                if (isInitialRender) {
                    ViewPU.create(new BuilderParamChild(this, {
                        builder: () => {
                            this.observeComponentCreation((elmtId, isInitialRender) => {
                                ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
                                Text.create('测试');
                                if (!isInitialRender) {
                                    Text.pop();
                                }
                                ViewStackProcessor.StopGetAccessRecording();
                            });
                            Text.pop();
                        }
                    }, undefined, elmtId));
                }
                else {
                    this.updateStateVarsOfChildByElmtId(elmtId, {});
                }
                ViewStackProcessor.StopGetAccessRecording();
            });
        }
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}
class BuilderParamChild extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.builder = undefined;
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
        if (params.builder !== undefined) {
            this.builder = params.builder;
        }
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
    }
    aboutToBeDeleted() {
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    initialRender() {
        this.builder().hitTestBehavior.bind(this)(HitTestMode.None);
    }
    rerender() {
        this.updateDirtyElements();
    }
}
ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent());
loadDocument(new BuilderParamTestDemo(undefined, {}));
ViewStackProcessor.StopGetAccessRecording();
//# sourceMappingURL=Index.js.map
```

对应这个问题,官方的解释是

1.`ArkUI` 没有类似安卓基类组件。
2.目前 `ArkUI` 组件是没有具体的类型,也不支持组件继承。
3.如果需要给自定义构建方法添加属性,只能是套一层容器组件之后再给容器组件设置属性
4.从语法规范上来讲,`BuilderParam` 的方法类型就是 `() => void`

话虽然这样说,但我还是提出了疑问,那么有没有那种单纯的容器组件, 不管是用 `Row`,还是 `Column` 或者其他功能容器,这里的含义都蛮奇怪的。

回答是,暂时没有。希望官方以后还是考虑一下这个,虽然我包个 `Row/Column` 是可以,但是感觉怪怪的。

```typescript
@Component
struct BuilderParamChild {
  @BuilderParam
  builder: () => void;

  build() {
    Column() {
      this.builder()
    }.hitTestBehavior(HitTestMode.None)
  }
}
```

### 状态装饰器

在给组件定义参数的时候,会遇到这个参数不必须设置,但后续需要根据情况给它一个默认值。

在 `Flutter` 中,我们可以通过定义参数为可空,然后在后续流程中判断这个参数是否为 `null`,再给它默认值。

在 `ArkTS` 中我第一反应是这样写:

```typescript
maxDragOffset: number | null = null;
```

但是,当这个参数如果用 `@Prop` 等状态装饰器修饰的时候,它是不允许简单类型和复杂类型的联合类型。这会引起很多奇怪的问题,在 `api9` 上面各种 `carsh`,但是 `api10` 看起来是支持了(顺便说说,`api9` 和 `api10` 的相同代码,效果不一样的情况比比皆是)。而且 `ide` 的错误定位也很奇怪,比如我在做另一个组件 `LikeButton` 的时候,错误堆栈直接误导了我好久,最后排除法才搞好的。

```
Js-Engine: ark
page: pages/Index.js
Error message: ObservedPropertySimple value must not be an object
Stacktrace:
at ObservedPropertySimple (/mnt/disk/jenkins/ci/workspace/chipset_pipeline_release/china_compile/component_code/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:2179:2179)
at SynchedPropertySimpleOneWayPU (/mnt/disk/jenkins/ci/workspace/chipset_pipeline_release/china_compile/component_code/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:3304:3304)
at CirclePainter (like_button/src/main/ets/painter/CirclePainter.ets:10:29)
at anonymous (like_button/src/main/ets/components/LikeButton.ets:461:35)
```

修复记录 [fix on api9 · HarmonyCandies/like_button@eefe49d (github.com)](https://github.com/HarmonyCandie ... c417e8fbdbcad01ba49)

所以你可以这样写,通过是否为 `undefined`,来判断用户是否设置过这个参数。

```typescript
@Prop maxDragOffset: number = undefined;
```

## 吐槽一下

### api 9.0 和 api 10.0 的差距

相同的代码在 `9.0` 和 `10.0` 上面会有不同的效果,并报错信息不准确。当然也有可以写的语法就有问题,但是疑惑的是 `10.0` 上面可以但是 `9.0` 上面不可以。

为啥要兼容 `9.0` 呢? 因为大部分人现在都没法在 `10.0` 上面开发。

* 之前说的状态装饰参数不能为复杂混合类型,`9.0` 报错,`ide` 的报错堆栈误导。

[fix on api9 · HarmonyCandies/like_button@eefe49d (github.com)](https://github.com/HarmonyCandie ... c417e8fbdbcad01ba49)

* 对于超出 `Stack` 大小的组件,`alignContent: Alignment.Center` 在 `api 9.0` 上面并不能让它居中。

[fix api9 · HarmonyCandies/like_button@d53db6f (github.com)](https://github.com/HarmonyCandie ... f088110eb39df3a7c43)

只能利用 `position()` 来手动调整位置

* `api 9.0` 上面无法将类的方法作为参数传递,比如下面的代码。
  [fix error on api9 · HarmonyCandies/loading_more_list@bfe1523 (github.com)](https://github.com/HarmonyCandie ... a1910246c70438dac5e)

```typescript
@Entry
@Component
struct HomePage {
  @Builder
  buildWidget(click: () => void) {
    Text('点我').onClick((x) => {
      click();
    })
  }

  build() {
    Column(){
      TestPage(
        { builder: this.buildWidget, }
      )
    }.justifyContent(FlexAlign.Center).width('100%').height('100%')
  }
}

@Component
struct TestPage {
  @BuilderParam builder: (click: () => void) => void;
  test = new Test();

  build() {
    this.builder(this.test.onClick)
  }
}


class Test {
  onClick() {
    console.log('测试点击')
  }
}
```

### 源码

我们通过 `ArkTS` 编写的 `ArkUI`, 通过 `DevEco Studio` 只能看到一些 `xxx.d.ts` 文件,都是一些接口。想学习下官方组件是怎么实现的,只能到
https://gitee.com/openharmony/arkui_ace_engine/ 这里学习。这些都是 `C++` 编写的。

```
/foundation/arkui/ace_engine
├── adapter                       # 平台适配目录
│   ├── common
│   └── ohos
├── frameworks                    # 框架代码
│   ├── base                      # 基础库
│   ├── bridge                    # 前后端组件对接层
│   └── core                      # 核心组件目录
```

我们编写的 `ArkTS` 会被转换成 `js`,然后通过  `NAPI(Native API)` 与 `C/C++` 之间的交互。

[Native API在应用工程中的使用指导 (openharmony.cn)](https://docs.openharmony.cn/page ... napi-guidelines.md/)

> 在 `OpenHarmony` 中,`C API` 中的 ` N-API` 接口可以实现 `ArkTS/TS/JS`与 `C/C++` 之间的交互。

由于组件绘制布局逻辑都在 `ace_engine` 里面,这意味着,普通开发者只能根据官方已提供的 `组件` 和 `Api` 进行组合,扩展。跟 `Flutter` 中可以利用 `RenderObject` 来做自定义布局绘制相比,大大降低了三方开发者的可操作性。

### OpenHarmony三方库中心仓

* 微软:[NuGet Gallery | Home](https://www.nuget.org/)
* Flutter:
  [Dart packages (flutter-io.cn)](https://pub-web.flutter-io.cn/)
* OpenHarmony https://ohpm.openharmony.cn/

使用下来,体验非常糟糕,用一句话形容就是

> whosyourdaddy

![DM_20231219092555_011.jpg](https://forums-obs.openharmony.c ... tlzjcaiczzbnqll.jpg "DM_20231219092555_011.jpg")

![DM_20231219092555_013.jpg](https://forums-obs.openharmony.c ... ewkv5ywmpnviijy.jpg "DM_20231219092555_013.jpg")

* 审核速度之慢,申请组织用了  `1` 星期,组件用了 `3` 天,感觉审核人员只在周二和周五上班,做二休三,可以内荐我去上班吗?
* 上传组件,拒绝没有任何的提示理由。
* 发邮件联系,没有任何的回复,没有一个能够沟通的方式。

![DM_20231219092555_014.jpg](https://forums-obs.openharmony.c ... 2t3t0tzds64t7tw.jpg "DM_20231219092555_014.jpg")

不知道华为的同学能看到吗? 给一个正常的沟通渠道呀。

## 考试一下

对于新的事物,新的东西,学习是不可避免的。有时间的童鞋可以去参加一下 OpenHarmony 的考试,对自己掌握. OpenHarmony 知识也是一种激励。

### 免费

![DM_20231219092555_015.jpg](https://forums-obs.openharmony.c ... i9bhdngmi9hfumh.jpg "DM_20231219092555_015.jpg")

* [HarmonyOS应用开发者基础认证-华为开发者学堂 (huawei.com)](https://developer.huawei.com/con ... /101666948302721398)
* [HarmonyOS云开发基础认证-华为开发者学堂 (huawei.com)](https://developer.huawei.com/con ... /101669098009823737)
* [HarmonyOS应用开发者高级认证-华为开发者学堂 (huawei.com)](https://developer.huawei.com/con ... /101684223987951077)

![DM_20231219092555_016.jpg](https://forums-obs.openharmony.c ... syix5cukdcyl5ji.jpg "DM_20231219092555_016.jpg")

### 收费

收费的话就看个人需求了

* [HCIA-HarmonyOS应用开发能力工程师-华为开发者学堂 (huawei.com)](https://developer.huawei.com/con ... /101665624627448188)
* [HCIP-HarmonyOS应用开发高级工程师-华为开发者学堂 (huawei.com)](https://developer.huawei.com/con ... /101666770725933116)

## 论坛

[OpenHarmony开发者论坛](https://forums.openharmony.cn/) , 大家有什么问题可以在上面提,也经常会有一些活动可以参加。可以看到虽然 `OpenHarmony` 虽然还有各种各样的问题,但是官方也是在很努力地为大家提供更好的体验。遥想 `Flutter 1.0.0` 之前,我们开发者还不是要啥啥没有,吐槽归吐槽,给 `OpenHarmony` 时间,相信它会变的更好。

> 如果华为能够推出预览的 `sdk` 和 `设备`,让三方开发者更早的接触 `api10` ,进行适配那就更好了。现在大部分人还是只能在 `api9` 上面做开发和学习,但是这 `2` 者的区别太大了。通过吸引更多的三方开发者贡献,一个开源项目才可以更好地成长和演进,获得更多的创新和支持,最终造福于整个社区。

## 结语

恍惚间,感觉又回到了 `5` 年前,那个时候刚创建 [FlutterCandies](https://github.com/fluttercandies),发布的第一个
[解决官方组件问题的扩展组件](https://juejin.cn/post/6844903713887240206), 那时候我们的群里面只有 `2` 个人。有时候感觉,程序员其实是一种孤独的动物,渴望能有志同道合的人一起学习讨论,哪怕是摸摸鱼吹吹水也好。如果你对. OpenHarmony 开发感兴趣,欢迎加入我们;如果你想贡献. OpenHarmony 社区,也欢迎加入我们;

![DM_20231219092555_017.jpg](https://forums-obs.openharmony.c ... po2pblekk1eowak.jpg "DM_20231219092555_017.jpg")

爱 OpenHarmony ,爱 `糖果`,欢迎加入[Harmony Candies](https://github.com/HarmonyCandies),一起生产可爱的.OpenHarmony 小糖果[![DM_20231219092555_018.jpg](https://forums-obs.openharmony.c ... obnf8gxxnhh6dkn.jpg "DM_20231219092555_018.jpg")
QQ群:981630644](https://qm.qq.com/q/ajfsyk2RcA)

![DM_20231219092555_019.jpg](https://forums-obs.openharmony.c ... u1twhjwi0i7tnw7.jpg "DM_20231219092555_019.jpg")
[/md]
作者: 9bf699919a6811ee8d055794b3832d58    时间: 2023-12-22 09:15
法的空间? 真人?
作者: zmtzawqlp    时间: 2023-12-28 10:16
回复 9bf699919a6811ee8d055794b3832d58: 难道还有假的。哈哈
作者: 马迪    时间: 2024-1-31 10:57
滑动效果很丝滑




欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/) Powered by Discuz! X3.5