diff --git a/arkoala-arkts/arkui/src/Application.ts b/arkoala-arkts/arkui/src/Application.ts index 545a7afa06c8f5e8a4ea1b012afe7128261ec5e8..6820d38ad17d6fc3ae34c265bc648e95b710c2bc 100644 --- a/arkoala-arkts/arkui/src/Application.ts +++ b/arkoala-arkts/arkui/src/Application.ts @@ -29,6 +29,7 @@ import { Routed } from "./handwritten" import { deserializeAndCallCallback } from "./generated/peers/CallbackDeserializeCall" import { Deserializer } from "./generated/peers/Deserializer" import { NativeLog } from "./NativeLog" +import { updateLazyItems } from "./RewrittenLazy" setCustomEventsChecker(checkArkoalaCallbacks) @@ -248,6 +249,7 @@ export class Application { for (const detachedRoot of detachedRoots.values()) { detachedRoot.value } + updateLazyItems() if (partialUpdates.length > 0) { // If there are pending partial updates - we apply them one by one and provide update context. for (let update of partialUpdates) { diff --git a/arkoala-arkts/arkui/src/LazyForEach.ts b/arkoala-arkts/arkui/src/LazyForEach.ts index 2a6429ac8057e9d86a4eacaf3e220b24c48cbc55..770468c5b87ff5a8c2d32a24ebc6a08bf7c0a033 100644 --- a/arkoala-arkts/arkui/src/LazyForEach.ts +++ b/arkoala-arkts/arkui/src/LazyForEach.ts @@ -135,7 +135,7 @@ class LazyForEachIdentifier { * @returns item offset of LazyForEach in parent's children */ /** @memo */ -function getOffset(parent: PeerNode, id: KoalaCallsiteKey): int32 { +export function getOffset(parent: PeerNode, id: KoalaCallsiteKey): int32 { let offset = 0 for (let child = parent.firstChild; child; child = child!.nextSibling) { // corresponding DataNode is attached after the generated items diff --git a/arkoala-arkts/arkui/src/LazyItemNode.ts b/arkoala-arkts/arkui/src/LazyItemNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9c070a58a4c02614fdd60953f35ed16afcdf764 --- /dev/null +++ b/arkoala-arkts/arkui/src/LazyItemNode.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IncrementalNode, Disposable } from "@koalaui/runtime" +import { PeerNode, LazyItemNodeType, PeerNodeType } from "./PeerNode" +import { KoalaCallsiteKey } from "@koalaui/common" +import { nullptr, pointer } from "@koalaui/interop" + +/** + * LazyItemNode is the root node of an item in LazyForEach. + * LazyForEach items are never attached to the main tree, but stored in a separate pool in LazyForEach. + */ +export class LazyItemNode extends IncrementalNode { + constructor(parent: PeerNode) { + super(LazyItemNodeType) + this._container = parent + this.onChildInserted = (node: IncrementalNode) => { + if (!node.isKind(PeerNodeType)) { + return + } + const peer = node as PeerNode + peer.reusable ? peer.onReuse() : peer.reusable = true + } + this.onChildRemoved = (node: IncrementalNode) => { + if (!node.isKind(PeerNodeType)) { + return + } + const peer = node as PeerNode + if (!peer.disposed) { + peer.onRecycle() + } + } + } + private _container: PeerNode + + /** + * Supports Reusable through redirecting requests to the parent node. + */ + reuse(reuseKey: string, id: KoalaCallsiteKey): Disposable | undefined { + return this._container.reuse(reuseKey, id) + } + + recycle(reuseKey: string, child: Disposable, id: KoalaCallsiteKey): boolean { + return this._container.recycle(reuseKey, child, id) + } + + getPeerPtr(): pointer { + const peer = this.firstChild + return peer?.isKind(PeerNodeType) ? (peer as PeerNode).getPeerPtr() : nullptr + } +} diff --git a/arkoala-arkts/arkui/src/PeerNode.ts b/arkoala-arkts/arkui/src/PeerNode.ts index 55c27ecf267ddfab9817c0cdcf23b04fd79d53d9..729abe9890abf19944498c893dc69caa71f5bab1 100644 --- a/arkoala-arkts/arkui/src/PeerNode.ts +++ b/arkoala-arkts/arkui/src/PeerNode.ts @@ -22,7 +22,8 @@ import { ReusablePool } from "./ReusablePool" export const PeerNodeType = 11 export const RootPeerType = 33 -export const LazyForEachType = 13 +export const LazyForEachType = 13 // corresponds to DataNode in LazyForEach +export const LazyItemNodeType = 17 // LazyItems are detached node trees that are stored privately in LazyForEach const INITIAL_ID = 1000 export class PeerNode extends IncrementalNode { @@ -37,7 +38,7 @@ export class PeerNode extends IncrementalNode { private _onRecycle?: () => void // Pool to store recycled child scopes, grouped by type private _reusePool?: Map - private _reusable: boolean = false + reusable: boolean = false getPeerPtr(): pointer { return this.peer.ptr @@ -55,7 +56,7 @@ export class PeerNode extends IncrementalNode { } onReuse(): void { - if (!this._reusable) { + if (!this.reusable) { return } if (this._onReuse) { @@ -139,7 +140,7 @@ export class PeerNode extends IncrementalNode { // TODO: rework to avoid search let peer = findPeerNode(child) if (peer) { - peer._reusable ? peer!.onReuse() : peer._reusable = true // becomes reusable after initial mount + peer.reusable ? peer!.onReuse() : peer.reusable = true // becomes reusable after initial mount let peerPtr = peer.peer.ptr if (this.insertMark != nullptr) { if (this.insertDirection == 0) { diff --git a/arkoala-arkts/arkui/src/RewrittenLazy.ts b/arkoala-arkts/arkui/src/RewrittenLazy.ts new file mode 100644 index 0000000000000000000000000000000000000000..80a71d9cfe8e5762ca64ab09af8f4301827ef9cb --- /dev/null +++ b/arkoala-arkts/arkui/src/RewrittenLazy.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { __id, ComputableState, contextNode, GlobalStateManager, Disposable, memoEntry2, remember, rememberDisposable, rememberMutableState, StateContext } from "@koalaui/runtime"; +import { getOffset, IDataSource } from "./LazyForEach"; +import { pointer } from "@koalaui/interop"; +import { PeerNode } from "./PeerNode"; +import { InternalListener } from "./DataChangeListener"; +import { LazyItemNode } from "./LazyItemNode"; +import { setNeedCreate } from "./ArkComponentRoot"; + +let globalLazyItems = new Set>() +export function updateLazyItems() { + for (let node of globalLazyItems) { + node.value + } +} + +/** @memo */ +export function LazyForEach(dataSource: IDataSource, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + keyGenerator?: (item: T, index: number) => string, +) { + let changeCounter = rememberMutableState(0) + const parent = contextNode() + const offset = getOffset(parent, __id()) + let listener = remember(() => { + let res = new InternalListener(parent.peer.ptr, changeCounter) + dataSource.registerDataChangeListener(res) + return res + }) + const changeIndex = listener.flush(offset) // first item index that's affected by DataChange + + // Entering this method implies that the parameters have changed. + let pool = rememberDisposable(() => new LazyItemPool(parent), (pool?: LazyItemPool) => { + pool?.dispose() + }) + + /** + * provide totalCount and callbacks to the backend + */ + let createCallback = (index: int32) => { + return pool.getOrCreate(index, dataSource.getData(index), itemGenerator) + } + // LazyForEachOps.Sync(parent.getPeerPtr(), dataSource.totalCount() as int32, createCallback, pool.prune) +} + +class LazyItemPool implements Disposable { + private _activeItems = new Map>() + private _parent: PeerNode + disposed: boolean = false + + constructor(parent: PeerNode) { + this._parent = parent + } + + dispose(): void { + if (this.disposed) return + + for (let node of this._activeItems.values()) { + node.dispose() + globalLazyItems.delete(node) + } + this.disposed = true + } + + get activeCount(): int32 { + return this._activeItems.size as int32 + } + + getOrCreate( + index: int32, + data: T, + /** @memo */ + itemGenerator: (item: T, index: number) => void, + ): pointer { + if (this._activeItems.has(index)) { + const node = this._activeItems.get(index)! + if (node.recomputeNeeded) { + console.log(`recomputeNeeded: ${index}`) + } + return node.value.getPeerPtr() + } + + const manager = GlobalStateManager.instance + const node = manager.updatableNode(new LazyItemNode(this._parent), + (context: StateContext) => { + const frozen = manager.frozen + manager.frozen = true + setNeedCreate(true) // ensure synchronous creation of CustomComponent + memoEntry2( + context, + index, // using index to simplify reuse process + itemGenerator, + data, + index + ) + setNeedCreate(false) + manager.frozen = frozen + } + ) + + this._activeItems.set(index, node) + globalLazyItems.add(node) + return node.value.getPeerPtr() + } + + /** + * prune items outside the range [start, end] + * @param start + * @param end + */ + prune(start: int32, end: int32) { + if (start > end) return + this._activeItems.forEach((node, index) => { + if (index < start || index > end) { + node.dispose() + this._activeItems.delete(index) // Delete in-place + globalLazyItems.delete(node) + } + }) + } +}