In every modern app, some sort of state is managed across app restarts. One such example are user preferences. Suppose you have a settings page in your application where users can check/uncheck preferences.
You would mostly have an interface like the following:
interface UserPreferences {
optionOne: boolean;
optionTwo: boolean;
...
optionN: boolean
}
I am now trying to provide a global state variable that stores and serves the user preferences. For that, I have created an Angular service with a signal of type UserPreferences
:
@Injectable({
providedIn: 'root'
})
export class PreferencesService {
public preferences = signal({ optionOne: true, optionTwo: false, ..., optionN: false } as UserPreferences);
}
Displaying that data in a component is also rather easy:
<ion-checkbox
[ngModel]="service.preferences().optionOne"
(ngModelChange)="service.preferences().optionOne = $event"
></ion-checkbox>
<ion-checkbox
[ngModel]="service.preferences().optionTwo"
(ngModelChange)="service.preferences().optionTwo = $event"
></ion-checkbox>
<ion-checkbox
[ngModel]="service.preferences().optionN"
(ngModelChange)="service.preferences().optionN = $event"
></ion-checkbox>
With this, it is possible to update the global object.
However, the data is only stored in-memory. To persist this data and make the user preferences usable across several app restarts, one has to use some sort of storage. One such storage solution would be the PreferencesAPI
of @capacitor
, which is an async API. (Please note that using window.localStorage
is not safe as webview storage might get deleted by the mobile operating system)
Issue 1
Loading the preferences from the storage and supplying them to the signal is not possible easily, as await can only be used in async functions. Meaning, something like this is not possible:
import { Preferences } from '@capacitor/preferences';
@Injectable({
providedIn: 'root'
})
export class PreferencesService {
public preferences = signal(await this.getPreferences());
async getPreferences(): Promise<UserPreferences> {
const { value } = await Preferences.get({ key: this.storageKey });
if (value) {
return JSON.parse(value);
}
const newPreferences: UserPreferences = {
optionOne: true,
optionTwo: false,
optionN: true
};
await this.store(newPreferences);
return newPreferences;
}
}
Omitting the await
also does not work, as the signal’s type would be of Promise<UserPreferences>
, which is tedious to deal with.
Issue 2
When setting the value in the template via ngModelChange
, I would like automatically call a function that stores the changed UserPreferences
with the PreferencesAPI
.
Possible, but not favoured solution
A workaround for both issues would be creating a signal for every user setting instead of relying on an object.
Something like this:
@Injectable({
providedIn: 'root'
})
export class PreferencesService {
public optionOne = signal(true);
public optionTwo = signal(true);
public optionN = signal(false);
constructor() {
effect(async () => {
Preferences.set({ key: 'optionOne', value: JSON.stringify(this.optionOne()) });
...
});
}
}
And when starting the application, the values of the signals could be provided inside the app.component.ts
file.
But with every new option a new signal instance variable has to be declared and in the effect hook the call to Preferences.set(...)
has to be done, which is rather cumbersome.
I would prefer the initial approach with the usage of an object UserPreferences
.
Is there any way this can be achieved? What is the best practice regarding this issue?