likes
comments
collection
share

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

作者站长头像
站长
· 阅读数 7

前言

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

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

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

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

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

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

下拉刷新

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

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

在鸿蒙中你则可以使用 github.com/HarmonyCand… 来实现。

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

安装

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

ohpm install @candies/pull_to_refresh

参数

PullToRefreshIndicatorMode

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

配置参数

参数类型描述
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);
  }
}
  • 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
};

回调

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;

onReachEdge

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

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

使用

导入引用

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

定义配置

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

使用 PullToRefresh

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

@BuilderParam装饰器:引用@Builder函数-快速入门-入门-HarmonyOS应用开发


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

自定义下拉刷新效果

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

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

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

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

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

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

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

创建项目

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

package
--example

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

example
--package

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

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

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

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

创建组件演示项目

创建一个项目。

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

创建组件项目

创建一个 Static Libray (至于其他 Module 是什么意思,请自行查看文档) Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

创建好的目录长这样子

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

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

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

其他字段,请查看 OpenHarmony三方库中心仓

一个完整的 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": {}
}

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

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

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

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

{
  "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"
  }
}

发布

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

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

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

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

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

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

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

@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 })
    }
  }
}

运行会报找不到 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

意思就是你只能写成下面的这种形式。虽然说 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 })
    }
  }
}

@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 })
    }
  }
}

但是实际中写一个自定义组件的时候,会有这种需求。需要为 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)
  }
}

错误堆栈如下:

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 中也能看到对应的代码。


"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 是可以,但是感觉怪怪的。

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

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

状态装饰器

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

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

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

maxDragOffset: number | null = null;

但是,当这个参数如果用 @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)

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

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

@Prop maxDragOffset: number = undefined;

吐槽一下

api 9.0 和 api 10.0 的差距

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

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

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

fix on api9 · HarmonyCandies/like_button@eefe49d (github.com)

  • 对于超出 Stack 大小的组件,alignContent: Alignment.Centerapi 9.0 上面并不能让它居中。

fix api9 · HarmonyCandies/like_button@d53db6f (github.com)

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

@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 文件,都是一些接口。想学习下官方组件是怎么实现的,只能到 gitee.com/openharmony… 这里学习。这些都是 C++ 编写的。

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

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

Native API在应用工程中的使用指导 (openharmony.cn)

OpenHarmony 中,C API 中的 N-API 接口可以实现 ArkTS/TS/JSC/C++ 之间的交互。

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

OpenHarmony三方库中心仓

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

whosyourdaddy

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

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

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

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

考试一下

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

免费

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

收费

收费的话就看个人需求了

论坛

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

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

结语

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)

鸿蒙,爱糖果,欢迎加入Harmony Candies,一起生产可爱的鸿蒙小糖果Flutter到鸿蒙,不是有手就行吗? (下拉刷新)QQ群:981630644

Flutter到鸿蒙,不是有手就行吗? (下拉刷新)