Capacitor storage plugin does not seem to hold data on iOS when app is force-killed

I am using the Storage plugin on one of my pages to get the user’s gender from an input:

x.page.html:

 <ion-select
                [(ngModel)]="userMale"
                [ngModelOptions]="{standalone: true}"
                (ngModelChange)="onChangeGender(coeff); onCalcPoints(coeff);"
                interface="popover"
              >
                <ion-select-option [value]="true">Male</ion-select-option>
                <ion-select-option [value]="false">Female</ion-select-option>
              </ion-select>

x.page.ts

  ngOnInit() {
    this.checkGender();
  }

  setGender = async () => {
    const gender = (this.userMale) ? 'male' : 'female';

    await Storage.set({
      key: 'gender',
      value: gender
    });
  };

  checkGender = async () => {
    if (typeof this.userMale === 'boolean') {
      const { value } = await Storage.get({ key: 'gender' });
      this.userMale = (value === 'male') ? true : false;
    }
  };

  onChangeGender(form: NgForm): void {
    this.coeffService.male = this.userMale;
    this.onCalcPoints(form);
    this.setGender();
  }

But when I force kill the app on an iPhone, and then go back to the app the data does not seem to have been held.

First off, apologies for the giant wall of text. There probably isn’t a direct, straightforward answer to your question in here, but if you need something to ponder while you wait for better answers, continue on.

I don’t know which if any of these things are actually responsible for the behavior you’re seeing, and you’re totally free to ignore my opinions, because they are admittedly arbitrary rules that are designed solely to prevent me from making mistakes that I have made in the past.

That being said,

  • I only read from device storage (whatever the implementation) once per app run. checkGender breaks that rule.
  • I also never touch device storage from pages, only services.
  • Finally, I never bind multiple handlers to the same event. You’ve got two on ngModelChange here.

I would be interested to know if rearchitecting the code to follow these rules magically designs away your problem here. There are two reasons that I think it might.

  1. I don’t know if there is an officially-defined execution order when multiple handlers are present on an event binding, but I strongly feel that acting as if it were totally unspecified is the safest thing for app code to do. If we invert the order of the assignment to userMale and the call to onChangeGender, I think we would get surprising behavior. (Incidentally, I think you are likely to run into trouble attempting to represent gender as a mere boolean). So here’s a situation where you might be inadvertently storing stale data.

  2. The liberal use of async and await (another thing I avoid, largely because of situations like this) obscures this, but setGender returns a Promise that is silently ignored by onChangeGender. You have no guarantee that checkGender has completed (either its read or its write) when onChangeGender returns.

In summary, I would redesign this so that:

  • all interaction with storage happens only in a service
  • reads happen only at app startup (or the first time the data is requested)
  • gender is stored as a string, not a boolean
  • do the assignment to userMale manually in your ngModelChange handler instead of banana-binding (ngModel), so you know when it happens

I applaud your decision to test this by force-quitting the app, because it exposes the futility of attempting to protect device storage referential integrity at write time. The reason I have the “do not read from storage more than once per app run” rule is that it allows me to blissfully ignore Promises from write operations. I don’t have to worry about the following scenario:

A. “foo” is sitting in storage.
B. live data becomes “bar”.
C. initiate write to storage for “bar”.
D. read from storage.
E. maybe we get new “bar”, maybe we get old “foo”.

If I never do D, the problem in E can’t happen. I never do D, because the only time I read from storage would be before B.

There is an alternative strategy here, which would be to ensure that C completes before D is allowed to start. I reject this strategy for two reasons, one of which you shined a light on here. First, it needlessly blocks the CPU. The live service, which should be the only source of truth for all the pages in the app, already knows that the new value is “bar”. It doesn’t have to wait to write it to storage and read it back again. The second reason is that it’s always possible that the app will get force quit while the write is pending, and there is absolutely no way to either block that or know about it from inside the app, so it’s pointless to try to address it directly.

1 Like

Sorry for the late reply and thank you so much for such a detailed reply! I had a very careful read and it inspired me to change my code in that:

  1. The functions that read/write to Storage are now in a service
  2. Any gender is changed from boolean to string
  3. The gender variable is checked not as truthy or falsey but their string value

Everything seems to work now, thank you so very much for your help!

1 Like