NativeScript / nativescript-angular

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

APP_INITIALIZER won't work with delayed promises #1487

Open Peluko opened 6 years ago

Peluko commented 6 years ago

Make sure to check the existing issues in this repository

Checked.

If there is no issue for your problem, tell us about it

I'm trying Angular's APP_INITIALIZER to initialize app configuration before app launches (SQLite reading). Angular's documentation states that it will wait until the promises on APPINITIALIZER are resolved to start the application. But on Nativescript you always obtain the error 'Bootstrap promise didn't resolve_' whenever you use a delayed Promise on APP_INITIALIZER. This simple code produces the error:

function testInitializer(): Promise<void> {
    return new Promise((resolve: () => void) =>
        setTimeout(() => resolve(), 10)
    );
}

@NgModule({
    bootstrap: [
        AppComponent
    ],
    imports: [
        NativeScriptModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: () => testInitializer, multi: true }
    ],
    schemas: [
        NO_ERRORS_SCHEMA
    ]
})
export class AppModule { }

However, if you return a resolved promise, it works. For example, if you change the initialization funtion to:

function testInitializer(): Promise<void> {
    return Promise.resolve();
}

it boots correctly.

I think that the problem is with the following code on platform-common.js, which doesn't wait for promises before checking the value of bootstrapPromiseCompleted:

https://github.com/NativeScript/nativescript-angular/blob/5e8c0921668a87f68a0f70b9622dc5a2c25a6224/nativescript-angular/platform-common.ts#L165-L197

Which platform(s) does your issue occur on?

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

It can be easily reproduced on Playground: APP_INITIALIZER_PROMISES

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

The above mentioned Playground does it well.

Is there any code involved?

vakrilov commented 6 years ago

Hey @Peluko - you are indeed correct. Тhe angular bootstrap is done in the launchEvent. The NativeScript framework expects that this event handler will return the root view for the app (inside args.root). Usually, the angular bootstrap resolves synchronously and so we are able to get the resulting root view and pass it to NativeScript (trough args.root). Having async app initializer causes the bootstrap promise to resolve async and so there is no view that we can give to the NativeScript framework to render.

Currently, there is no workaround for using async app initializer. Maybe you can do the async job inside your AppComponent or in an angular route resolver service.

There are a couple approaches we have discussed for overcoming this:

  1. Extending logic handling the application launchEvent inside NativeScript, so that you can return a promise.
  2. Changing the angular bootstrap so that if the promise is not resolved immediately - we return a temporary empty view and than when the promise is resolved - we replace the application root with the view returned form the async bootstrap.
kspearrin commented 5 years ago

I am also running into this issue.

@vakrilov Regarding point 2, couldn't the splash screen continue to show until it is resolved?

vedranstanic82 commented 5 years ago

Maybe you can do the async job inside your AppComponent or in an angular route resolver service.

Hi @vakrilov - we are trying to grab a 100kb json file from a server as the app is initializing/loading. This json will be used in the initialisation of the NS TabView template pages as the source of data for all tabs. The TabView has the page-router-outlet mechanism as we started with the NT Tab template:

<page-router-outlet *tabItem="{title: 'Scheduler', iconSource: getIconSource('search')}" name="schedulerTab"> </page-router-outlet>

How can we make sure that we first have the json data from the server grabbed, so that we can proceed with the app initialisation, or in other words with using that data to draw out all of the tab screens/pages? Currently the TabView inits all tabs immediately without waiting for the getJSON to complete and we get JS errors (undefined) in the tab ts files. Do you have a recommended approach, and if so if you could provide some details or a link on how to do it that would be super appreciated.

Thanks!

Vedran

vakrilov commented 5 years ago

Have you looked into the resolver service? Maybe you can add one to your default path (you should have a page-router-outlet outlet as the root of your app). Other approach might be to have a loader component in your default path and trigger the loading inside it. Once the data is loaded you can navigate with clearHistory to your tabbed component.

vedranstanic82 commented 5 years ago

Thanks @vakrilov, we went with your suggested approach (resolver) and we added it to the first child page route of the tabview component (1st tab page route).

The tabview component is a little bit special it seems in how it connects the routes with the tabs and the whole initialisation of it, although we are new to NS so it could be just us. We tried the "loading component" approach and then navigating to the tabbed component once resolver was done, but having a tabview with child page-router-outlets seems to collide with the app root child-page-router. Perhaps we need to have nested routers in that case? I'm sure this is confusing to read and a Playground example would be much better, but the 1st tab resolver approach seems to work so far with the resolver attached to the tabview child route (1st tab route), although it is yet to be seen how nice of a transition we will be able to make as the app loads for a longer period of time and then renders the 1st tab.

That's why the loading component approach sounds attractive, but not sure how to get that transition from the loading component to the tabview working. The tabview breaks in terms of rendering itself...does the tab view need to sit in its own module, or do we need to have two page-router-outlets, one for the original app and one for the tabs inside the tabview? thnx

DimitarTachev commented 5 years ago

A similar error is reproducible during live sync when debugging navigations (Livesync bootstrap promise didn't resolve). https://github.com/NativeScript/nativescript-angular/blob/master/nativescript-angular/platform-common.ts#L328

Steps to reproduce: 1) npm i nativescript@5.1.0 -g && tns create appNG –ng && cd appNG 2) tns debug ios 3) Set breakpoint in ngOnInit of item-detail.component.ts and tap on an item to hit it. 4) Edit a css file in order to trigger the livesync process. 5) You will get the Livesync bootstrap promise didn't resolve error in the Simulator.

As far as I see, the error is caused by the same (<any>global).Zone.drainMicroTaskQueue(); call. We could think about an alternative solution like locking a local object instead of relying on the zone.

csimpi commented 4 years ago

Pls, fix this, there's no way to initialize things before the APP would start which is a huge issue when we're talking real Apps

flodaniel commented 4 years ago

we are also coming across this issue now. we expected it would be easy to implement, because in angular it is. but sadly impossible in nativescript to have a nice implementation. :/

csimpi commented 4 years ago

@Firetrip Currently the only way to solve this is the route resolvers. I'm not comfortable with it but there's no other chance.

flodaniel commented 4 years ago

@csimpi but how do you handle the wait time? The splashscreen already disappears at this point :(

edusperoni commented 4 years ago

You could create a "splash" view which displays the app logo and acts like an extended splash screen, so your first route would be splash screen which immediately goes to the next route with a resolver. AFAIK there's no way to extend the native splash screen. Even in the native space the recommended way is to create a new activity with the splash (https://devdeeds.com/how-to-create-a-5-seconds-splash-screen-in-android/)

As soon as your app is loaded android will hide the splashscreen. Best thing you can do is show an artificial one to circumvent this native limitation.

flodaniel commented 4 years ago

I tried that, but you can see the animation. This is my route structure and instead of a resolver i do the async logic and the navigation in the SplashComponent.

const routes: Routes = [
    {
        path: "",
        component: SplashComponent,
    },
    {
        path: "pre-login",
        loadChildren: () => import("~/app/pre-login/pre-login.module").then((m) => m.PreLoginModule)
    },
    {
        path: "post-login",
        loadChildren: () => import("~/app/post-login/post-login.module").then((m) => m.PostLoginModule),
        canActivate: [LoginActivate]
    },
];