Skip to main content

How settings work

A setting is something the user chooses and the app remembers, such as the theme mode. In SMF a setting belongs to the module, or to the role, whose code needs it. That owner keeps the state of the setting, and three roles do the rest without knowing the owner:

The preferences role remembers the setting, the settings screen role shows it to the user, and the localization role translates its label. A module can leave each of them optional by only using the role. Then, without preferences, the setting lasts as long as the app runs. Without a settings screen, the app gets no row for it. Without the localization role, its label is in English.

The state belongs to the owner​

The owner keeps the setting in an object of its own, such as a ValueNotifier in a file that it generates. Widgets listen to that object and change the setting through it. They never touch the preferences, as the screens of a feature never touch the DI container.

Preferences​

The preferences role keeps the settings of the app, and no secrets. Nothing in the preferences is encrypted, and a backup of the device carries them, so a token, a password, an API key or an encryption key never goes there. A secret belongs in the secure storage of the platform.

The template of the role generates lib/core/preferences/app_preferences.dart with the AppPreferences interface, initPreferences() and createAppPreferences(). The module that provides the role contributes the implementation, as the providers of the other service roles do.

An owner does not ask for the preferences. It gives the role a restorer, a function that takes the AppPreferences, through the socket PreferencesRole.restorers, and the app hands the preferences to every restorer when it starts:

A restorer keeps to these rules:

  • It is a function of the type void Function(AppPreferences preferences).
  • It runs in the platform phase of bootstrap(), before the DI container is filled and before the first frame. So the setting has its saved value when the first screen builds, and when a router first asks a guard that reads it.
  • It reads the settings of its owner, puts them into the state of its owner, and keeps the preferences there for the writes to come.
  • It is synchronous and awaits nothing. The reads are synchronous, so it has done all of its work when it returns. The role does not wait for an asynchronous restorer and does not catch what such a one throws.
  • It keeps the current value when nothing is saved under its key. A write before the preferences are open changes only memory, so state that a test sets before bootstrap() survives the start.
  • It may run more than once. A test runs initPreferences() again to see what the next launch reads, and each restorer then gets the preferences that were opened anew.

The role calls each restorer on its own, since the owners know nothing of each other. What one throws keeps no other from restoring and does not stop the start-up. The app starts with the defaults of that owner, and in debug mode it prints what was thrown.

A restorer is the way to the preferences that every app has, for a module and for the template of a role. Neither calls createAppPreferences(), and only bootstrap() calls initPreferences(); a rule of the role checks both in the generated code. The tests of an app are free to call both, to read a key and to open the preferences again. In an app with a DI container, the role also registers the preferences there as a lazy singleton. So a service can take them as a dependency of its factory, and the composition file of a feature can resolve them.

A key is <owner id>.<setting>, such as theme.mode, where the owner is the module or the role whose code keeps the setting. The ids of modules and roles differ, so the keys of owners that keep to this form do not collide. Nothing checks it: two owners that save under one key overwrite each other's setting.

If the preferences cannot be opened, initPreferences() fails, and bootstrap() with it.

The settings screen​

The settings screen role connects the owners of settings with the screen that shows them. An owner contributes a SettingsEntry, which names the widget of the setting in a file that the owner generates. The widget has a const constructor that requires no arguments, so the screen can create it knowing nothing but its class. It reads and changes its setting itself.

The module that provides the role is a module with a route that shows the screen, and it names that route with a SettingsScreenRoute. So the role requires the router. Every provider promises the same screen:

  • It shows every entry once, in the order of the role: the entries of the modules, in the order the modules were chosen, and then those of the templates of roles.
  • It shows them one below the other in a list that scrolls, on a Material. Each entry gets the same width and no limit on its height, so an entry may be a ListTile or a column of them.
  • It creates the widget of each entry through an import of its file with a prefix of its own. Two owners may name their widgets alike, and they still do not clash.

How the user gets to the screen is up to the provider. settings makes its route a destination of the main navigation, which an app with a layout shows.

The contract harness checks that the provider creates the widget of every entry, so the tests of an owner read its entry from the role and never from the files of a provider. What only a running app shows, such as the order and the scrolling, SMF's CI checks in running apps; see Contributing.

The theme mode, a setting of a role​

The theme role is built from these pieces, and it shows what the template of a role is for. The role splits the theme between its provider and its template:

The provider owns the look. It generates lib/core/theme/app_theme.dart with createLightTheme(context) and createDarkTheme(context), and that is all the role asks of it.

The template owns the mode, the same with every provider. It generates appThemeMode, the controller of the mode, and AppThemeModeScope, the widget around the root of the app through which the root reads the mode. It gives the preferences role the restorer of the mode, which is saved under the key theme.mode. In an app with a settings screen, it generates ThemeModeSetting and gives the settings screen role an entry for it, and it gives the localization role the texts of that entry. It also gives the root MaterialApp its theme, its darkTheme and its themeMode.

The role requires the preferences role, so an app with a theme always remembers the mode, and it only uses the other two.

The three arguments of the root take one value each. A module that sets one of them cannot be in an app with the theme role, and smf create reports the two values. A provider whose look depends on state of its own, such as a color that the user picks, puts an inherited widget with that state among the root wrappers and reads it from the context that its two functions get. For a setting of that kind it contributes an entry of its own.

The language of the app is a setting of a role in the same way. The template of the localization role keeps the language that the user chose in appLocale, gives the preferences role its restorer, and gives the settings screen role the entry that chooses it; see Texts and languages.

Next​