Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Reusable and recyclable NativeScript views #7469

Open
@edusperoni

Description

@edusperoni

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions