NativeScript / nativescript-angular

Integrating NativeScript with Angular
http://docs.nativescript.org/angular/tutorial/ng-chapter-0
Apache License 2.0
1.21k stars 240 forks source link

TabView crashes upon tab navigation #857

Open tsonevn opened 7 years ago

tsonevn commented 7 years ago

From @nilsmehlhorn on June 14, 2017 11:52

Hey, I've got the exact same issue as described in #4317 after updating my project to NS 3.0, however adding a simple ngIf did not do the trick for me. As I have not changed any of my code since the update I'm fairly certain that it is a bug in the TabView. I'll fill out the form just like @AlvSovereign did, you'll see its almost the same:

Which platform(s) does your issue occur on?

Android

Please provide the following version numbers that your issue occurs with:

CLI: 3.0.3 Cross-platform modules: 3.01 Runtime(s): Node: 7.8.0, npm: 4.2.0 Plugin(s):

Please tell us how to recreate the issue in as much detail as possible.

Scroll across to the 3rd tab in the tab view and the app crashes. These tabs are dynamically added via Angular's *ngFor directive on the tag.

Interestingly enough, there is no data being shown on the first and second tab. When it gets to the third, there is an image from the API call it makes, but as it comes to view, it crashes.

It is the same for me. The first two tabs are showing no data initially though they should (only if add something, as my tabs are presenting dynamic lists in them). When I get to the third tab, I see its contents being displayed correctly for a second, then the application crashes with the following error message:

An uncaught Exception occurred on "main" thread.
com.tns.NativeScriptException:
Calling js method destroyItem failed

Error: Expected
org.nativescript.widgets.StackLayout{6c8498a V.E..... .....I. 0,0-0,0} to equal
org.nativescript.widgets.StackLayout{390e391 V.E..... .....I. 0,0-768,928}
File "file:///data/data/...../tns-core-modules/ui/tab-view/tab-view.js, line: 64, column: 16

Is there code involved? If so, please share the minimal amount of code needed to recreate the

This is how my tab-view code looks:

<TabView #tabView *ngIf="registers" [selectedIndex]="selectedTabIndex" (selectedIndexChanged)="tabViewIndexChange(tabView.selectedIndex)"
    androidSelectedTabHighlightColor="#f44336">
    <ng-template ngFor [ngForOf]="registers" let-reg>
        <StackLayout *tabItem="{title: reg.name}">
            <kard-board-register [model]="reg"></kard-board-register>
        </StackLayout>
    </ng-template>
</TabView>

So some equality check inside the PageAdapterImpl.destroyItem fails, but I could not yet figure out why. If this cant be resolved I guess I have to roll back to NS 2.5

EDIT I've tested the issue further: The TabView seems to hold instantiated versions of the current tab and the ones next to the current one (if there is one on each side). Now, when the third tab is selected the instance for the first tab is disposed, hence the call to the destroyItem method. I think this behaviour is related to the NS 3.0 updates regarding view recycling (or rather the new view life cycle). Yet, unfortunately the instance passed to destroyItem does not equal the item the tab-view class is keeping track of. Furthermore, as already mentioned, the first two tab-items are not getting rendered at all.

The source for the destroyItem method has the following note attached:

// Note: this.owner._removeView will clear item.view.nativeView.
// So call this after the native instance is removed form the container. 
// if (item.view.parent === this.owner) {
//     this.owner._removeView(item.view);
// }

Would it be possible that this call order isn't met? https://github.com/NativeScript/NativeScript/blob/master/tns-core-modules/ui/tab-view/tab-view.android.ts#L78

EDIT 2 So, I've tested the issue without fetching the models for my tabs from the backend. Binding three tabs directly is working without problems - all tabs are rendered correctly and I am able to switch between them. I even found a fix for making it work with the remote tab models: Instead of the suggested *ngIf="registers" I used *ngIf="registers.length > 0". It solves the issue for the initial display, yet when I add a tab while the tab-view is displayed, the same behaviour occurs again.

Copied from original issue: NativeScript/NativeScript#4380

tsonevn commented 7 years ago

Hi @nilsmehlhorn, First of all, thank you for your interest in NativeScirpt. In NativeScript the TabView for Android will keep alive one tab on the left and on the right side of the currently selected tab. If you navigate to the third tab the first one could be recycled and this could lead to the problem with the navigation.

For those case, we created platform specific property called androidOffscreenTabLimit, which allows setting up the number of tabs that should be retained to either side of the current tab in the view hierarchy in an idle state. For further info, you could review the API ref. For example:

<TabView androidOffscreenTabLimit="5">
    <StackLayout *tabItem="{title: 'Profile', iconSource: '~/icon.png'}" >
        <ListView [items]="items">
            <template let-item="item">
                <Label [text]="item.itemDesc"></Label>
            </template>
        </ListView>
    </StackLayout>
    <StackLayout *tabItem="{title: 'Stats'}">
        <Label text="Second tab item"></Label>
    </StackLayout>
    <StackLayout *tabItem="{title: 'Settings'}">
        <Label text="Third tab item"></Label>
    </StackLayout>
</TabView>

Could you verify, whether this setup will resolve this behavior?

tsonevn commented 7 years ago

From @nilsmehlhorn on June 15, 2017 11:13

Hey @tsonevn, setting the property did in fact fix the issue. Still, I wonder why I would have to set the property. For one thing, the issue seems to be related to dynamic loading (or adding/removing) tab-items and in addition the property will, to my understanding, circumvent the view recycling.

My application should allow for handling different lists in parallel tabs. These lists (and therefore the corresponding tabs) are meant to be dynamically added and removed. Furthermore the tabs are initially retrieved from a server. Why wouldn't a dynamic tab-view work? Am I supposed to adjust the androidOffscreenTabLimit all the time to comply with my tab count?

How did this work with NS 2.5, I guess because back then the view recycling wasn't working how it does now, right?

EDIT I'd like to add, that even with the androidOffscreenTabLimit property set to 5, my tabs go blank when I add a tab-item dynamically.

EDIT 2 I'm attaching a sample project (based on the one provided by you in the other issue) exemplifying the issue. I guess it should be reproducible at your end with this project. I think I'll roll back to NS 2.5 in the meantime. Tabs.zip

tsonevn commented 7 years ago

Hi @nilsmehlhorn, Excuse me for the delay in the reply, To resolve your case you could bind androidOffscreenTabLimit and change dynamically the value of this property to be equal to this.tickers.length. This will keep alive all tabs and the navigation issue should be resolved.

HTML

<ActionBar title="My App" class="action-bar">
    <ActionItem text="Add" (tap)="addTab()"></ActionItem>
    <ActionItem text="Remove" (tap)="removeTab()"></ActionItem>
</ActionBar>
<TabView [androidOffscreenTabLimit]="adOffScrValue">
    <ng-template ngFor let-ticker [ngForOf]="tickers">
        <StackLayout *tabItem="{title: ticker.symbol}">
            <Label [text]="ticker.name"></Label>
        </StackLayout>
    </ng-template>
</TabView>

TypeScript

import { Component, OnInit } from "@angular/core";

import { Item } from "./item";
import { ItemService } from "./item.service";

@Component({
    selector: "ns-items",
    moduleId: module.id,
    templateUrl: "./items.component.html",
})
export class ItemsComponent implements OnInit {
    tickers: Array<any>;
    adOffScrValue:number = 7;

    constructor(private itemService: ItemService) { }

    ngOnInit(): void {
        this.itemService.getItems()
            .subscribe(tickers => this.tickers = tickers)
    }

    addTab(): void {
        this.tickers.push({ symbol: "TEST", name: "Test Entry" });
        this.adOffScrValue=this.tickers.length
    }

    removeTab(): void {
        this.tickers.splice(this.tickers.length - 1, 1);
    }
}
tsonevn commented 7 years ago

From @nilsmehlhorn on June 20, 2017 12:17

Hey @tsonevn, were you really able to verify a flawless execution with the code you are providing? The issue is still persisting for me and overall I really don't like the fix. For me, the tab-view implementation seems to need a fix. Circumventing the view recycling by arguably introducing overhead in any component that hosts a (dynamic) tab-view can't be the solution.

I made a video of the behavior I am seeing with your code. The corresponding value for androidOffscreenTabLimit is visible in the action bar. tabs_video_issue4380.zip

The issues to be seen in the video in detail:

tsonevn commented 7 years ago

Hi @nilsmehlhorn, I reviewed again your problem and have to confirm that the above-described points are still valid issues for your case. However, in my opinion, this is related to the way the binding works in NativeScript Angular 2 and the way how the UI is loaded. At this time I was unable to find a temporary solution for this case and you could keep track on it for further info.