Description
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 #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 #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.