当前位置:   article > 正文

Flutter 到 OpenHarmony,不是有手就行吗? (下拉刷新)_flutter openharmony

flutter openharmony

前言

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

作为发展了 5 年的 FlutterCandies 社区,我们已拥有 70+Flutter 组件。我们当然也不会止步于 Flutter 。我们希望把我们的 Flutter 组件也能带到 OpenHarmony 生态当中,HarmonyCandies 便是为了这一刻。

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

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

使用的 ide 版本为 DevEco Studio 4.0 Release OpenHarmony v4.0 Release (2023-10-26) ,开发 sdkapi 9,当然也适配了 api 10

下拉刷新

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

Flutter 中你可以通过
pull\_to\_refresh\_notification 来实现一个可以自定义任何效果的下拉刷新。

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

安装

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

ohpm install @candies/pull_to_refresh

参数

PullToRefreshIndicatorMode

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

配置参数

参数类型描述
maxDragOffsetnumber最大拖动距离(非必填)
reachToRefreshOffsetnumber到达满足触发刷新的距离(非必填)
refreshOffsetnumber触发刷新的时候,停留的刷新距离(非必填)
pullBackOnRefreshboolean在触发刷新回调的时候是否执行回退动画(默认 false)
pullBackAnimatorOptionsAnimatorOptions回退动画的一些配置(duration,easing,delay,fill)
pullBackOnErrorboolean刷新失败的时候,是否执行回退动画(默认 false)
  • maxDragOffsetreachToRefreshOffset 如果不定义的话,会根据当前容器的高度设置默认值。
/// 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);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • pullBackAnimatorOptions 的默认值如下:
/// 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
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

回调

onRefresh

触发的下拉刷新事件

/// 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;
  • 1
  • 2
  • 3
  • 4
  • 5
onReachEdge

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

/// Whether we reach the edge to pull refresh
onReachEdge: () => boolean = () => true;
  • 1
  • 2

使用

导入引用

import {
  PullToRefresh,
  pull_to_refresh,
  PullToRefreshIndicatorMode,
} from '@candies/pull_to_refresh'
  • 1
  • 2
  • 3
  • 4
  • 5

定义配置

@State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller();
  • 1

使用 PullToRefresh

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

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, };
    })
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

自定义下拉刷新效果

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

/// The current drag offset
dragOffset: number = 0;
/// The current pull mode
mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial;
  • 1
  • 2
  • 3
  • 4

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

@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')
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

学废了

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

创建发布一个组件

创建组织

先到 OpenHarmony 三方库中心仓 上面注册个账号,到 个人中心 =》组织管理 中,申请一个组织。这个组织名字以后要用到,因为普通三方作者,是不能使用 ohos 前缀的。

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

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

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

创建项目

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

package
--example
  • 1
  • 2

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

example
--package
  • 1
  • 2

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

但是 OpenHarmony 三方库中心仓 却要求,有点难顶啊。

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

创建组件演示项目

创建一个项目。

创建组件项目

创建一个 Static Libray (至于其他 Module 是什么意思,请自行查看文档)

创建好的目录长这样子

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

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

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

{
  "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": {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

export { MainPage } from './src/main/ets/components/mainpage/MainPage'
  • 1
引用组件项目

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

{
  "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"
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
发布

在准备发布之前,请先阅读 贡献三方库 下面内容。

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

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

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

@Provide/@Consume

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

最开始设计 pull\_to\_refresh 的时候,想着跟 Flutter 中一样,父组件里面存放管理下拉刷新的状态,然后子组件里面监听状态,达到局部刷新的效果。

第一版的设计结构如下:

  • CustomWidget 中提供了 @Provide('a')
  • CustomWidgetChild 中使用 @Consume('a') 获取状态变化。
@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 })
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

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

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

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180

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

@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 })
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

@Builder/@BuilderParam

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

@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 })
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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

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


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

  build() {
    this.builder().hitTestBehavior(HitTestMode.None)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

错误堆栈如下:

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

从生成的 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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

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

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

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

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

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

  build() {
    Column() {
      this.builder()
    }.hitTestBehavior(HitTestMode.None)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

状态装饰器

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

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

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

maxDragOffset: number | null = null;
  • 1

但是,当这个参数如果用 @Prop 等状态装饰器修饰的时候,它是不允许简单类型和复杂类型的联合类型。这会引起很多奇怪的问题,在 api9 上面各种 carsh,但是 api10 看起来是支持了(顺便说说,api9api10 的相同代码,效果不一样的情况比比皆是)。而且 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

修复记录 fix on api9 · HarmonyCandies/like\_button@eefe49d (github.com)

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

@Prop maxDragOffset: number = undefined;
  • 1

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:https://qr21.cn/FV7h05

入门必看:https://qr21.cn/FV7h05
1.  应用开发导读(ArkTS)
2.  ……

HarmonyOS 概念:https://qr21.cn/FV7h05

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

如何快速入门:https://qr21.cn/FV7h05
1.  基本概念
2.  构建第一个ArkTS应用
3.  构建第一个JS应用
4.  ……

开发基础知识:https://qr21.cn/FV7h05
1.  应用基础知识
2.  配置文件
3.  应用数据管理
4.  应用安全管理
5.  应用隐私保护
6.  三方应用调用管控机制
7.  资源分类与访问
8.  学习ArkTS语言
9.  ……

基于ArkTS 开发:https://qr21.cn/FV7h05
1.  Ability开发
2.  UI开发
3.  公共事件与通知
4.  窗口管理
5.  媒体
6.  安全
7.  网络与链接
8.  电话服务
9.  数据管理
10.  后台任务(Background Task)管理
11.  设备管理
12.  设备使用信息统计
13.  DFX
14.  国际化开发
15.  折叠屏系列
16.  ……

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/329720
推荐阅读
相关标签
  

闽ICP备14008679号