Ionic 3.3.0 app start working before initialization finished


#1

hello,
I am using Ionic v3.3.0, and am having a problem with the application initialization/ startup.

I have placed initialization code in the constructor of “MyApp” (the root component) in app.component.ts. and set the “this.platform.ready()” as the last step in the initialization sequence.

see the code snippet below

 constructor(private platform: Platform, private menu: MenuController, private splash: SplashScreen, private statusb: StatusBar, 
    private device: Device, private storage: Storage, private oauth2: Oauth2, private events: Events, private user: User) {
    // Check if the user has already seen the tutorial
    this.storage.ready()
      .then(() => this.setRoot())
      .then(() => this.getKeys())
      .then(() => this.setLocale())
      .then(() => this.setId())
      .then(() => this.setAccessToken())
      .then(() => this.setAnonymous())
      .then(() => this.splash.hide())
      .then(() => this.statusb.styleDefault())
      .then(() => this.setCurrentPAge())
      .then(() => this.platform.ready());

these operations are mainly concerned with the initialization/restoration of the application wide parameters/configurations (which had been stored in the storage). for example, the access token, the login status ,etc…

I have designed/implemented thus that there is an automatic fetching of data from the server upon entering the default page,

seen below

 constructor(private nav: NavController, private events: Events, private gabriel: Gabriel, 
    private valet: MsgsValet, private user: User, private popup: Popup){
    //set the handlers
    this._insert = (msgs) => {this.insert(msgs);};
    this._fetch = (ids) => {this.fetch(ids);};
    this._remove = (ids) => {this.remove(ids);};
    this._override = (od) => {this.override(od);};

    //subscribe
    this.events.subscribe(Cons.EVT_HI_MSGS, this._insert);
    this.events.subscribe(Cons.EVT_MSGS_CHANGED, this._fetch);
    this.events.subscribe(Cons.EVT_MSGS_DELETED, this._remove);
    this.events.subscribe(Cons.EVT_MSGS_OVERRIDE, this._override); 
    this.fetch([]);
    this.user.currentPage = Pages.Messages;
  }

the syndrome of problem is that the first attempt of fetching data upon startup always fails.

I traced into the code, and found out that the data fetching operation had occurred even before the initialization finishing.
parameters such as login status, access token, and user id, etc are still “undefined”, which have caused a “401” error.

I tried to maintain a strict execution sequence by adopting the async-await mechanism in the initialization/restore functions(see below), yet had not had any effect.

private async setRoot(){ 
    let noTutorial = await this.storage.get(Cons.noTutorial);

    if(noTutorial) {
      this.rootPage = TabsPage;
    }else{
      this.rootPage = TutorialPage;
    }  
  }

  private async getKeys(){
    this.keys = await this.storage.keys()
  }

  private async setLocale(){
    let locale;
    if(this.hasKey(Cons.locale)){
      locale = await this.storage.get(Cons.locale);
    }else{
      locale = 'zh';
      this.storage.set(Cons.locale, locale)
    }
    this.user._locale = locale;
  }

so, my question: how may I be able to control the initialization and startup sequence to make sure that initialization always finishes before service starts.

thanks


#2

Put as little computation in the constructors as possible. Their role is to declare variables so they are not null. The in app.component.ts, once your Promise chain completes, you do
.then( () => this.nameOfService.initializeThisService() );

and that method provides the “official” initialization of the service. Also, you probably already know this, but an Event will arrive asynchronously, so just because your Promise chain completes does not mean the Event has already arrived at your service. So your initialization needs to take that into account.


#3

I would be surprised if storage was ever ready before the platform was.


#4

I got this impression from the “ionic-conference-app”;

in this model app, they just get data from storage at the very beginning; even without bothering to check the readiness of the storage. so I assume it might be better, as a precaution, to check the readiness of storage before getting any data from it,
they only issued “this.platform.ready()” after some data had been fetched from storage and the root Page has been set,
so I guess I can put the “platform.ready()” after “storage.ready()”. also, I “defer” the readiness of the platform to the last step so that initialization might finish.


#5

Not to be rude, but I also think something is wrong with this design. Too many checks on app level, while nothing is supposed to be known YET.
And obviously what you have is an ASYNC issue, some view try to load a data before it is defined/accessed in the template (.html).

First why not make an if on the constructor of RootPage, like if user is authenticated, something like this:

 constructor(private platform: Platform, private menu: MenuController, private splash: SplashScreen, private statusb: StatusBar, 
    private device: Device, private storage: Storage, private oauth2: Oauth2, private events: Events, private user: User) {

let user = your auth check;

if (this.user.auth === ok)

{

  // Check if the user has already seen the tutorial
    this.storage.ready()
      .then(() => this.setRoot())
      .then(() => this.getKeys())
      .then(() => this.setLocale())
      .then(() => this.setId())
      .then(() => this.setAccessToken())
      .then(() => this.setAnonymous())
      .then(() => this.splash.hide())
      .then(() => this.statusb.styleDefault())
      .then(() => this.setCurrentPAge())
      .then(() => this.platform.ready());

}

...

#6

well, at this moment, the application is in the process of “waking up” , and is still in a kind of “dullness”. meaning everything is blank.
what you do is try to restore the “context” of the moment when the app was shutdown. only after that, can you do any meaning check.
as for the tutorial, I did it in the first step (this.setRoot())
you are right about data loading. what I am trying to do is to stop that before the “context” is restored.


#7

@Jingzhou Sorry i don’t understand what you mean. You have sleep states in your app to reload data or not? Do I understand now?


#8

well, let us say, you had powered off your mobile phone. and now just powered it on, and clicked the app, so the app need to initialize and restore as much as possible to the “context” before power off.

I do not have any “sleep state”, sorry to have confused you


#9

MMM OK. This is a bit extreme case, so you use a mix of local storage of the smartphone then, and if no connection at all to 3/4G for example you use it by default. Then make a provider for that. You can also use Redux, which many people talk about in the forums, because it allows full control on a local database on the phone, with zero latency (like local phone storage but better). And then synchronize it with your online database if the phone goes connected / online with observables or promises that check if the smartphone is connected to internet.


#10

sorry, I do not quite catch up with you, but what is the “nameOfService.initializeThisService()” ?

below is the excerpt of my “app.module.ts”, what shall I put in the place of “nameOfService” ?

@NgModule({
  declarations: [
    MyApp,
    AboutPage,
   ...
  ],
  imports: [
    BrowserModule,
    HttpModule,
    IonicModule.forRoot(MyApp),
    IonicStorageModule.forRoot()
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    AboutPage,
   
 ...
  ],
  providers: [
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    Cons,
   ...
  ]
})
export class AppModule {}

also, as per your comment, I moved the data loading (this.fetch()) into “ionViewDidLoad()”, ( see below) hoping it could be deferred a bit further. yet the problem (401 error) still persist.

 ionViewDidLoad(){
    this.fetch([]);
    this.user.currentPage = Pages.Messages;
  }

#12

I would abstract out things like setRoot(), getKeys(), setLocale(), setId(), to a service. Stylistically, it looks as though you are doing too much inside app.component.ts. For example, my app contains a profileCreationService, that checks first for oAuth2 redirects, then checks for an auth credential in Storage, then builds a profile if it finds a credential from redirect or Storage, or returns Promise.resolve(false) if no such credential exists. This app is initialized by a one-line call inside app.component.ts.

I use Promise and Observable, not async/await, but isn’t there a technical issue with how you’re using that pattern? Shouldn’t it be

private async getKeys() {
   return this.keys = await this.storage.keys()
}

instead of what you have now? It looks as though, as currently written, getKeys() immediately returns a Promise<void>. So that might be the solution to your problem. But even if that’s the issue, I think you would benefit from putting more structure (i.e., more providers/services) into your code.


#13

I see what you mean…
I used to have a service named “settings” in which, I put all the “setXxx()”. then I realize I need more control on the execution sequence, I move them all into the root module constructor.
this is not a big deal in term of efficiency lose, because it is only executed once during startup

I resort to async-await also because it is more sequential and simple. below is one of my original setXxx()

private setAnonymous(){
    if(this.hasKey(Cons.anonymous)){
      this.storage.get(Cons.anonymous)
        .then(ano => this.user._anonymous = ano);
    }else{
      this.user.anonymous = true; 
    }
  }

you can see it could be synchronous/asynchronous depending on whether the value had been set in the storage, which renders the controlling of execution sequence very complex and inefficient in a context of Promise chain.

all in all, the point here is that we need a stricter control on execution sequence here…


#14

The issue is I don’t know the global point of your app. Gloablly, what i showed it a declaration in global (app.component.ts), then a local use. If you load native plugins results without auth, it leads to crap, to me.


#15

What is the data fetching operation, exactly?


#16

It is nothing more than just getting a list of all the messages, so that the default messages page can display all available messages upon entering, rather than user manually pushing a button after entering the page.

It is very similar to the behavior when you are entering your twitter account.

To a wider extent, You may consider it a kind of “initialization” of the messages list upon entering the page


#17

So it’s not part of the code you posted? No wonder nobody knows what the answer to your question is.


#18

Ok then, what you want is basically what any async app do with [(ngModel)] and *ngFor loop. But I guess you tried that, no?

And if the value doesn’t exist yet because of the async load, use the ? operator like in this kind of loop, to avoid crashes.
Assuming your data is well loaded in controller or service/provider as “items” (fit it with your own values instead of quantity & family), you can do something simple like that:

      <ion-list>
        <ion-item *ngFor="let item of items">
          {{item.quantity}} {{item.family}}
        </ion-item>
      </ion-list>

#19

The proper way to deal with this is to initialize what is being looped across with an empty array.


#20

@rapropos My bad yes, elvis operator won’t work inside an *ngFor loop. I update code. Or empty value before the constructor, if it’s not an array.


#21

well, “*ngFor” is not what I am expecting…

there is “anonymous mode” and “login mode” in the application, depending on the value of anonymous.

when it is “login mode” (anonymous == false"), the system will insert access token in the fetch([]). (see below, ); otherwise

   ...
    let headers = new Headers({'Content-Type': 'application/json'});
    // insert certification if login
    if(!this.user.anonymous){
      headers.append('Authorization', this.oauth2.token);
    }

the problem here is that the system can not distinguish between anonymous ==false and anonymous not set.

so, ideally, I want a mechanism to make sure that anonymous is set before entering above code, so we can logically be sure that !anonymous is anonymous==false;

I wonder if there is such a mechanism (an event, or something) that can procure a higher execution priority , so we can put any setting relevant code in it, and get executed before any actual service.