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

前言

五年前,有人告訴我,你可以錯(cuò)過其他技術(shù),但千萬不要錯(cuò)過 Flutter 。然而此刻,有人告訴我,如果你錯(cuò)過了 OpenHarmony,恐怕要錯(cuò)過下個(gè)時(shí)代了。

作為發(fā)展了 5 年的 FlutterCandies 社區(qū),我們已擁有 70+Flutter 組件。我們當(dāng)然也不會(huì)止步于 Flutter 。我們希望把我們的 Flutter 組件也能帶到 OpenHarmony 生態(tài)當(dāng)中,HarmonyCandies 便是為了這一刻。

Flutter 開發(fā)者的角度,盡可能提供相同 Api 的 OpenHarmony 組件。

本文默認(rèn)您已經(jīng)有一定的 OpenHarmony 開發(fā)經(jīng)驗(yàn),并且閱讀過以下內(nèi)容。

使用的 ide 版本為 DevEco Studio 4.0 Release OpenHarmony v4.0 Release (2023-10-26) ,開發(fā) sdkapi 9,當(dāng)然也適配了 api 10 。

下拉刷新

列表在一個(gè) App 中最常見的呈現(xiàn)方式,而下拉刷新是其常見的一種效果。

Flutter 中你可以通過
pull\_to\_refresh\_notification 來實(shí)現(xiàn)一個(gè)可以自定義任何效果的下拉刷新。

在.OpenHarmony 中你則可以使用 https://github.com/HarmonyCandies/pull\_to\_refresh來實(shí)現(xiàn)。

安裝

你可以通過下面的命令來下載安裝

ohpm install @candies/pull_to_refresh

參數(shù)

PullToRefreshIndicatorMode

export enum PullToRefreshIndicatorMode {
  initial, // 初始狀態(tài)
  drag, // 手勢(shì)向下拉的狀態(tài).
  armed, // 被拖動(dòng)得足夠遠(yuǎn),以至于觸發(fā)“onRefresh”回調(diào)函數(shù)的上滑事件
  snap, // 用戶沒有拖動(dòng)到足夠遠(yuǎn)的地方并且釋放回到初始化狀態(tài)的過程
  refresh, // 正在執(zhí)行刷新回調(diào).
  done, // 刷新回調(diào)完成.
  canceled, // 用戶取消了下拉刷新手勢(shì).
  error, // 刷新失敗
}

配置參數(shù)

參數(shù) 類型 描述
maxDragOffset number 最大拖動(dòng)距離(非必填)
reachToRefreshOffset number 到達(dá)滿足觸發(fā)刷新的距離(非必填)
refreshOffset number 觸發(fā)刷新的時(shí)候,停留的刷新距離(非必填)
pullBackOnRefresh boolean 在觸發(fā)刷新回調(diào)的時(shí)候是否執(zhí)行回退動(dòng)畫(默認(rèn) false)
pullBackAnimatorOptions AnimatorOptions 回退動(dòng)畫的一些配置(duration,easing,delay,fill)
pullBackOnError boolean 刷新失敗的時(shí)候,是否執(zhí)行回退動(dòng)畫(默認(rèn) false)
  • maxDragOffsetreachToRefreshOffset 如果不定義的話,會(huì)根據(jù)當(dāng)前容器的高度設(shè)置默認(rèn)值。
/// 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 的默認(rèn)值如下:
/// 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
};

回調(diào)

onRefresh

觸發(fā)的下拉刷新事件

/// 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

是否我們到達(dá)了下拉刷新的邊界,比如說,下拉刷新的內(nèi)容是一個(gè)列表,那么邊界就是到達(dá)列表的頂部位置。

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

使用

導(dǎo)入引用

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 回調(diào)傳入,或者尾隨閉包初始化組件。

PullToRefresh(
    {
      refreshOffset: 150,
      maxDragOffset: 300,
      reachToRefreshOffset: 200,  
      controller: this.controller,
      onRefresh: async () => {
        return new Promise<boolean>((resolve) => {
          setTimeout(() => {
            // 定義的刷新方法,當(dāng)刷新成功之后,返回回調(diào),模擬 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}`;
      })
    }
    // 必須設(shè)置 edgeEffect
    .edgeEffect(EdgeEffect.None)
    // 為了使下拉刷新的手勢(shì)的過程中,不觸發(fā)列表的滾動(dòng)
    .onScrollFrameBegin((offset, state) => {
      if (this.controller.dragOffset > 0) {
        offset = 0;
      }
      return { offsetRemain: offset, };
    })
  }
}

自定義下拉刷新效果

你可以通過對(duì) ControllerdragOffsetmode 的判斷,創(chuàng)建屬于自己的下拉刷新效果。如果下拉刷新失敗了,你可以通過調(diào)用 Controllerrefresh() 方法來重新執(zhí)行刷新動(dòng)畫。

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

下面是一個(gè)自定義下拉刷新頭部的例子

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

學(xué)廢了

雖然練習(xí)時(shí)長(zhǎng)只有一個(gè)月,但通過編寫第一個(gè) ArtUI 組件,還是學(xué)到了不少東西。

創(chuàng)建發(fā)布一個(gè)組件

創(chuàng)建組織

先到 OpenHarmony 三方庫中心倉 上面注冊(cè)個(gè)賬號(hào),到 個(gè)人中心 =》組織管理 中,申請(qǐng)一個(gè)組織。這個(gè)組織名字以后要用到,因?yàn)槠胀ㄈ阶髡?,是不能使?ohos 前綴的。

比如我注冊(cè)的是組織名為 candies,組件為 pull_to_refresh。那么組件最終的名字就是 @candies/pull_to_refresh

最后用戶可以通過 ohpm install @candies/pull_to_refresh,來安裝使用組件。

為啥這個(gè)要先做,因?yàn)閷徍撕苈?/p>

創(chuàng)建項(xiàng)目

寫一個(gè)組件,必然也會(huì)給這個(gè)組件創(chuàng)建一個(gè)演示例子,在 Flutter 中發(fā)布一個(gè)組件,你可以使用下面的結(jié)構(gòu)。

package
--example

而在 OpenHarmony 里面你只能使用下面的結(jié)構(gòu),這樣才能方便你修改代碼。

example
--package

2 種結(jié)構(gòu)的區(qū)別是, package 下面肯定會(huì)需要加 READMELICENSE,但是 github,gitee 默認(rèn)只會(huì)顯示根目錄下面的 README,第二種結(jié)構(gòu)就要多復(fù)制一份到 example 目錄下面。

但是 OpenHarmony 三方庫中心倉 卻要求,有點(diǎn)難頂啊。

ide 啥時(shí)候支持下第一種結(jié)構(gòu)呀!

創(chuàng)建組件演示項(xiàng)目

創(chuàng)建一個(gè)項(xiàng)目。

創(chuàng)建組件項(xiàng)目

創(chuàng)建一個(gè) Static Libray (至于其他 Module 是什么意思,請(qǐng)自行查看文檔)

創(chuàng)建好的目錄長(zhǎng)這樣子

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

這里你需要把名字改成 @candies/pull_to_refresh(@你的組織名字/組件名字)

一個(gè)完整的 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": {}
}

組件項(xiàng)目中 Index.ets 是入口,用于導(dǎo)出組件。跟 Flutterlib 下面帶 library 組件名; 標(biāo)識(shí)的 dart 文件效果一樣。

export { MainPage } from './src/main/ets/components/mainpage/MainPage'
引用組件項(xiàng)目

要想 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"
  }
}

發(fā)布

在準(zhǔn)備發(fā)布之前,請(qǐng)先閱讀 貢獻(xiàn)三方庫 下面內(nèi)容。

閱讀操作完畢之后,你就可以打你的 har 包了。選中你的組件項(xiàng)目,在 Build 下面選擇 Make Module 你的組件名字。編譯完成之后,你就可以在組件項(xiàng)目路徑 build\default\outputs\default\ 中找到你即將發(fā)布的包。

最后執(zhí)行 ohpm publish xxx.har(xxx.har 為上傳包的本地路徑)。上傳成功之后,你就可以看到你的個(gè)人中心里面的消息和狀態(tài)了,耐心等待審核。

我遇到的上架的問題主要是組織名稱(當(dāng)然,這是我自己猜的,后面會(huì)聊到這個(gè)),ohos 不是普通三方開發(fā)者使用的前綴, ohos 的庫都在 OpenHarmony-TPC: OpenHarmony third party components (gitee.com)下面。按道理你可以 pr 到這個(gè)下面,并且加入到 ohos 中,再發(fā)布。當(dāng)然更歡迎大家能加入 candies 組織,大家一起生產(chǎn)有趣的小組件。

@Provide/@Consume

第一眼看到這個(gè)狀態(tài)管理裝飾器的時(shí)候,好親切的感覺。這不是就是 Flutter 里面的 (provider | Flutter Package (flutter-io.cn)) 嗎?

最開始設(shè)計(jì) pull\_to\_refresh 的時(shí)候,想著跟 Flutter 中一樣,父組件里面存放管理下拉刷新的狀態(tài),然后子組件里面監(jiān)聽狀態(tài),達(dá)到局部刷新的效果。

第一版的設(shè)計(jì)結(jié)構(gòu)如下:

  • CustomWidget 中提供了 @Provide('a')
  • CustomWidgetChild 中使用 @Consume('a') 獲取狀態(tài)變化。
@Entry
@Component
struct HomePage {

  @Builder
  builder2($$: { a: string }) {
    Text(`${$$.a}測(cè)試`)
  }


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

運(yùn)行會(huì)報(bào)找不到 Provide 的錯(cuò)誤。

通過分析由 ArkTS 生成的 js 文件(生成的 jsentry\build\default\cache\default\default@CompileArkTS\esmodule\debug 路徑下面) ,我們可以分析得出:
CustomWidgetChild 其父組件實(shí)際上是 HomePage,其內(nèi)部 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}測(cè)試`);
            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 創(chuàng)建出來的,但是它們依然沒有父子關(guān)系,這跟 Flutter 完全不是一套原理。

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

  @Builder
  builder2($$: { a: string }) {
    Text(`${$$.a}測(cè)試`)
  }

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

但是實(shí)際中寫一個(gè)自定義組件的時(shí)候,會(huì)有這種需求。需要為 BuilderParam 修飾的內(nèi)容的返回增加一些事件或者設(shè)置。比如下面例子,為 BuilderParamChildbuilder 的返回增加 hitTestBehavior 設(shè)置。我這里將 builder 的返回修改為了 CommonMethod<any>(組件都繼承于該類,里面是一些公共的屬性,事件),雖然這樣可以讓編輯器有提示,并且不報(bào)錯(cuò),但是運(yùn)行起來依然會(huì)提示 hitTestBehavior 找不到。

@Component
struct BuilderParamTestDemo {
  build() {
    Column(){
      BuilderParamChild(){
        Text('測(cè)試')
      }
    }
  }
}


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

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

錯(cuò)誤堆棧如下:

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 中也能看到對(duì)應(yīng)的代碼。

"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('測(cè)試');
                                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

對(duì)應(yīng)這個(gè)問題,官方的解釋是

1.ArkUI 沒有類似安卓基類組件。
2.目前 ArkUI 組件是沒有具體的類型,也不支持組件繼承。
3.如果需要給自定義構(gòu)建方法添加屬性,只能是套一層容器組件之后再給容器組件設(shè)置屬性
4.從語法規(guī)范上來講,BuilderParam 的方法類型就是 () => void

話雖然這樣說,但我還是提出了疑問,那么有沒有那種單純的容器組件, 不管是用 Row,還是 Column 或者其他功能容器,這里的含義都蠻奇怪的。

回答是,暫時(shí)沒有。希望官方以后還是考慮一下這個(gè),雖然我包個(gè) Row/Column 是可以,但是感覺怪怪的。

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

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

狀態(tài)裝飾器

在給組件定義參數(shù)的時(shí)候,會(huì)遇到這個(gè)參數(shù)不必須設(shè)置,但后續(xù)需要根據(jù)情況給它一個(gè)默認(rèn)值。

Flutter 中,我們可以通過定義參數(shù)為可空,然后在后續(xù)流程中判斷這個(gè)參數(shù)是否為 null,再給它默認(rèn)值。

ArkTS 中我第一反應(yīng)是這樣寫:

maxDragOffset: number | null = null;

但是,當(dāng)這個(gè)參數(shù)如果用 @Prop 等狀態(tài)裝飾器修飾的時(shí)候,它是不允許簡(jiǎn)單類型和復(fù)雜類型的聯(lián)合類型。這會(huì)引起很多奇怪的問題,在 api9 上面各種 carsh,但是 api10 看起來是支持了(順便說說,api9api10 的相同代碼,效果不一樣的情況比比皆是)。而且 ide 的錯(cuò)誤定位也很奇怪,比如我在做另一個(gè)組件 LikeButton 的時(shí)候,錯(cuò)誤堆棧直接誤導(dǎo)了我好久,最后排除法才搞好的。

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)

修復(fù)記錄 fix on api9 · HarmonyCandies/like\_button@eefe49d (github.com)

所以你可以這樣寫,通過是否為 undefined,來判斷用戶是否設(shè)置過這個(gè)參數(shù)。

@Prop maxDragOffset: number = undefined;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容