Reusable and recyclable NativeScript views
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 25/100
- Issue 类型
- 功能
- 描述清晰度
- 需要澄清
- 活跃度
- 停滞
- 技术栈
- android, ios, typescript
- 领域
- mobile-dev
调研方向
首先阅读 NativeScript PR 3912 中引入的 native view 释放行为,并在链接的 TypeScript 和 Angular playground 中复现 detach/attach 场景。完成标准是定义可复用 view 的生命周期,使 native view 在 detach 时得以保留,并且可以在不重新创建的情况下 attach 到另一个 parent。
由索引模型根据 Issue 内容生成。
描述
Is your feature request related to a problem? Please describe.
Removing a NativeScript view from the visualtree results in all native views being disposed on Android. On iOS, some native views are not disposed, but their delegates are. This behavior seems to come from https://github.com/NativeScript/NativeScript/pull/3912 but I haven't found an explanation for why it is that way.
Since the view is destroyed, there is no way to do, for example:
const parent1 = stack1;
const parent2 = stack2;
const st = new StackLayout();
const bt = new Button()
st.addChild(bt);
parent1.addChild(st);
// st.android != null
parent1.removeChild(st);
// st.android == null
parent2.addChild(st);
// st.android != null
Without the view being destroyed and created again.
This also makes hard to use some plugins like nativescript-popup. We've tried creating an angular template and passing the NS views to the plugin, but once we detached the view from it's parent, the native views became null and the popup was empty. Our workaround was to manually use the native views that were created and set view.parent = null otherwise the app would crash when closed.
Describe the solution you'd like
NativeScript views could be reusable:
const parent1 = stack1;
const parent2 = stack2;
const st = new StackLayout();
st.reusable = true;
const bt = new Button()
st.addChild(bt);
parent1.addChild(st);
const native = st.android;
parent1.removeChild(st);
native === st.android // true
parent2.addChild(st);
native === st.android // true
This is different from https://github.com/NativeScript/NativeScript/issues/4189, since we're not just reusing the native views, but the whole NS view, so no properties have to be reset. Maybe an additional method (like view.inflate()) could be provided to inflate views without the need to attach to the "dom".
Additional context
I've only used core and angular flavors, so I'll contextualize it using them:
Angular/Vue/React/etc flavors of nativescript would create all their views as reusable. The angular renderer has destroyNode method that is unimplemented by nativescript-angular because the core already destroys the view when it's detached. This method is called when the view is not being used anymore, so the view can be safely detached and reused. I'm sure the other frameworks have similar approaches.
This necessity came up when I was starting to develop a recyclerview plugin with shared pools (which is possible natively, no need for this PR) and general use of nativescript-popup and I stumbled upon my views being destroyed whenever I tried to detach them.
Here's a playground of the current behavior: https://play.nativescript.org/?template=play-tsc&id=eorFFE&v=6
Here's what happens when we try detaching and attaching: https://play.nativescript.org/?template=play-ng&id=mXwdnl
Ideally, "create native view" should only output once for each label created by "Create Views" and none for "Move Views".
Partially inspired by: https://hackernoon.com/react-native-listview-performance-revisited-recycling-without-the-bridge-c4f62d18c7dd?gi=87e16d79933e
Use cases
Custom reusable heavy views/view tree in the core
Allows you to reuse a view or view tree anywhere. Examples: footer that is used in almost every screen only needs to instanced once. Same view pool for multiple ListViews. A conditional view that may be added or not to a ListView item (the current best practice is instantiate all views and set the visibility property, or using multiple templates, potentially increasing memory usage).
// viewrecyler.ts
export class ViewRecycler {
viewPool = [];
availableViews = [];
prefetch = 5;
constructor() {
for (let i = 0; i < this.prefetch; i++) {
this.generateReusableView();
}
}
generateReusableView() {
const view = new HeavyView(); // HeavyView could be a whole view tree
view.reusable = true;
view.inflate(); // force creation, we're prefetching.
this.viewPool.push(view);
this.availableViews.push(view);
return view;
}
getView() {
if (this.availableViews.length === 0) {
this.generateReusableView(); // if none available, generate a new one
}
return this.availableViews.pop();
}
storeView(detachedView: HeavyView) {
this.availableViews.push(detachedView);
}
}
export const viewRecycler = new ViewRecycler();
// page.ts
let recyclableView;
function navigatedTo() {
const container = getViewById(page, "viewcontainer");
recyclableView = viewRecycler.getView();
container.addChild(recyclableView);
}
function navigatingFrom() {
if (recyclableView) {
recyclableView.parent.removeChild(recyclableView);
viewRecycler.storeView(recyclableView);
recyclableView = null;
}
}
It's up to the developer to set/reset view properties. In the example above, all properties must be set on recyclableView = viewRecycler.getView();, or reset on viewRecycler.storeView(recyclableView);. When too many properties are being reset each time (defeating the purpose of recycling), it's recommended to add a new view pool.
ngFor/Repeater
What could be accomplished with this:
<Button *ngFor="let item of item;"></Button>
This is already possible, but every time you swap something, angular uses detach and insert, meaning you get lag. Workaround: use trackBy using index.
<Button *ngReusableFor="let item of items; uniqueKey:'myForKey'"></Button>
<Button *ngReusableFor="let item of items2; uniqueKey:'myForKey'"></Button>
ngReusableFor would have a service that stores views in myForKey. No matter how many items you add/remove from items and item2, the same views would be reused in both of them. caution: the same template has to be used for both fors.
Global templates
My favorite use case.
<GlobalTemplates>
<ng-template templateKey="reusablecard" let-item="item">
<CardView>
<StackLayout>
<Label text="item.title"></Label>
<Label text="item.content"></Label>
</StackLayout>
</CardView>
</ng-template>
</GlobalTemplates>
...
<!-- use inside a listview, ngfor, or any html -->
<ReusableTemplate key="reusablecard" context="item"></ReusableTemplate>
Every time you'd use that template, it'd be a recycled instance from somewhere in the app. You could have horizontal listviews inside a vertical listview and they all share the same pool of recycled components. You could also configure some prefetching (create 5 cards when the app starts and just use them over and over).
Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.
- 主要语言
- TypeScript
- 星标
- 25.7k
- 派生
- 1.7k
- 平均合并
- 1 天 5 小时
- 30 天内合并 PR
- 35
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
NativeScript/NativeScript 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 65/100
NativeScript/NativeScript#11068 ·
-
bug-pending-triage
难度 3/5 1-2 天 新手友好度 66/100
NativeScript/NativeScript#11436 ·
-
难度 4/5 3-5 天 新手友好度 45/100
NativeScript/NativeScript#11421 ·
-
bug-pending-triage
难度 3/5 1-2 天 新手友好度 52/100
NativeScript/NativeScript#11415 ·
-
bug-pending-triage
难度 3/5 1-2 天 新手友好度 48/100
NativeScript/NativeScript#11374 ·
查看 NativeScript/NativeScript 的全部 Issue
相似的 Issue
-
bug
难度 2/5 1-3 小时 新手友好度 72/100
-
难度 1/5 1 小时以内 新手友好度 85/100
-
check:passed streams:add
难度 2/5 1-3 小时 新手友好度 68/100
-
难度 2/5 1-3 小时 新手友好度 76/100
-
难度 2/5 1-3 小时 新手友好度 68/100