Skip to content

A library for providing declarative configuration of app settings

License

Notifications You must be signed in to change notification settings

4e6anenk0/settings_provider

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Settings Provider

Logo

A library for providing declarative configuration of app settings.

Features

  • Description of configuration settings using declarative properties;
  • It is convenient to separate settings from the project;
  • Handy methods and helper functions to manage settings;
  • Use the Setting widget to easily provide settings in your project.
  • Use the EnumProperty properties for the Enums and EnumPropertyBuilder to build the appropriate UI;
  • Use Config and ConfigBuilder to group settings by platform and use the groups of settings depending on the platform.

If you need the simplest implementation, read about the Settings widget:

If you want the most flexible and powerful solution, see about Config and ConfigBuilder:

If you need to store the Enum locally and the last value to build the appropriate UI, then see more about the Scenario property:

Also visit pub.dev:

Quick start. Example

Simple usage with SettingsModel

  1. settings.dart:
class GeneralSettings extends SettingsModel {
  @override
  List<BaseProperty> get properties => [isDarkMode, counterScaler];

  static const Property<bool> isDarkMode = Property(
    defaultValue: false,
    id: 'isDarkMode',
    isLocalStored: true,
  );

  static const Property<int> counterScaler =
      Property(defaultValue: 1, id: 'counterScaler', isLocalStored: true);
}
  1. main.dart:
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  var generalSettings = GeneralSettings();

  await generalSettings.init();

  runApp(
    Settings(
      model: generalSettings,
      child: const SimpleApp(),
    ),
  );
}
  1. access to the settings:
// get setting without subscription
context.setting<GeneralSettings>().get(property);
// or
Settings.from<GeneralSettings>(context).get(property);

// get setting with subscription
context.listenSetting<GeneralSettings>().get(property);
// or
Settings.listenFrom<GeneralSettings>(context).get(property);

// update setting
context.setting<GeneralSettings>().update(property);
// or 
Settings.from<GeneralSettings>(context).update(property);

More functionality usage with ConfigModel

  1. settings.dart:
class GeneralConfig extends ConfigModel {
  @override
  List<ConfigPlatform> get platforms => [ConfigPlatform.general];

  @override
  List<BaseProperty> get properties => [isDarkMode, counterScaler, name];

  static const Property<bool> isDarkMode = Property(
    defaultValue: false,
    id: 'isDarkMode',
    isLocalStored: true,
  );

  static const Property<int> counterScaler = Property(
    defaultValue: 1,
    id: 'counterScaler',
    isLocalStored: false,
  );

  static const Property<String> name = Property(
    defaultValue: "Jonh",
    id: 'name',
    isLocalStored: false,
  );
}
  1. main.dart:
void main() async {
  var generalConfig = GeneralConfig();

  await generalConfig.init();

  runApp(
    Config(
      providers: [
        SettingsProvider(
          model: generalConfig,
        ),
      ],
      child: ConfigAppForWeb(),
    ),
  );
}
  1. access to the settings:
// get setting without subscription
context.setting<GeneralConfig>().get(property);
// or
Config.from<GeneralConfig>(context).get(property);

// get setting with subscription
context.listenSetting<GeneralConfig>().get(property);
// or
Config.listenFrom<GeneralConfig>(context).get(property);

// update config
context.setting<GeneralConfig>().update(property);
// or 
Config.from<GeneralConfig>(context).update(property);

Getting started

The settings_provider library implements several concepts that are familiar to Flutter developers. Let's get acquainted with the concept of the library in a little more detail.

The settings_provider separates settings into local settings (delegated to the Shared Preference storage or other local storage) and equivalent current session settings. The settings_provider tries to keep settings synchronized between these levels. For this, the entire configuration is described in Property() objects, which are immutable, constant and declarative. Also, settings_provider automatically creates the required settings both locally (optionaly) and in the current session.

All interaction with the settings happens through the SettingsController.

Usage

1. The Settings() widget class. Example of the simplest usage

1.1. Properties

To create a configuration, the package provides a several classes that allows you to create a declarative configuration of settings:

  • Property - a simple declarative setting property
  • UIProperty- like property, but with a converter function to convert data into a readable format for the UI
  • EnumProperty - property with the ability to store Enum settings

Property only describes the setting and does not have to match your current or local settings configuration. This means you need only care about the default value in you app and whether this value should be stored in local storage (SharedPreferences by default).

For example, create the first property:

const counterScaler = Property(
    defaultValue: 1, 
    id: 'counterScaler', 
    isLocalStored: true,
);

defaultValue - the default value. It also specifies the type for the property.

id - key for value or name. It's better to call it the same as the variable.

isLocalStored - parameter that indicates whether data should be stored in local storage. Optional parameter. The default value is false.

1.2. Settings widget

In order to use the package in your project, you need to wrap the application creation function in Settings() widget at the top level. This will allow you to get the configuration before the application UI starts rendering. This is necessary for consistent creation of the app, so that the configuration of the session matches the data of the local storage.

The Settings() widget is responsible for implementing settings in the widget tree. To implement settings, you need to create a class based on SettingsModel and initialize this class asynchronously using the init() method:

var generalSettings = GeneralSettings();

await generalSettings.init();

Main function example:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  var generalSettings = GeneralSettings();

  await generalSettings.init();

  runApp(
    Settings(
      model: generalSettings,
      child: const SimpleApp(),
    ),
  );
}

1.3. Access to the settings

Once the settings have been implemented, you can refer to them to get them or update the values.

To do this, you need to refer to the static function from<T>() or listenFrom<T>(), which is available in the Settings class, and find the desired method to pass the property. You must specify the type of model you created previously, otherwise you will not be able to find your settings:

Settings.from<T>(context).get(property);

or:

Settings.listenFrom<T>(context).get(property);

A logical question arises... Do you need to create a new property with similar settings every time when you make a change to the settings? Yes, but you don't have to do it yourself ever.

If you want to make a change in your settings, you need to use the copyWith() method, which is well known to Flutter developers. For example, look how simple it is:

Settings.from(context).update(property.copyWith(defaultValue: newValue));

Here are the methods available to you for working with the settings:

  // 1
  Future<void> update<T>(Property<T> property);
  // 2
  T get<T>(Property<T> property);
  // 3
  void setForSession<T>(Property property);
  // 4
  Future<void> setForLocal<T>(Property property);
  // 5
  Future<void> match();
  // 6
  Map<String, Object> getAll(); 
  1. update(property) - to update setting values both locally (if true) and for the current session;
  2. get(property) - to get the setting value;
  3. setForSession(property) - to set the value in the current session. This has no effect on saving the value in local storage, but causes listeners to be notified of the change in setting. This can be useful for changes only in the current session to a value that is stored locally, or for deferred saving to reduce the number of local storage accesses
  4. setForLocal(property) - sets the value for local storage only. Changes will be synchronized only after restarting the application (in a new session)
  5. match() - update (synchronization) of all the data of the current session with the data of the local storage. Relevant for making changes after calls to setForSession()
  6. getAll() - returns Map() of all settings relevant for the current session

1.4. Helpers for access to settings

For convenience, the library implements helpers functions for accessing settings. These are available via context references and make it even easier to work with a simple Settings().

Example:

context.setting<T>().get(property);

Instead:

Settings.from<T>(context).get(property);

Or:

context.listenSetting<T>().get(property);

Instead:

Settings.listenFrom<T>(context).get(property);

Choose what is more convenient for you.

2. The MultiSettings() widget class. Example of the group usage

The library also provides the ability to create separate groups of settings. They will have separate areas of responsibility, that is, they will have their own controller and notification system. Next, we will go through the steps that must be taken in order to use this option in your project:

2.1. Let's create separate configurations from properties:

First configuration:

class FirstSettings extends SettingsModel {
  @override
  List<BaseProperty> get properties => [isDarkMode, counterScaler];

  static const Property<bool> isDarkMode = Property(
    defaultValue: false,
    id: 'isDarkMode',
    isLocalStored: true,
  );

  static const Property<int> counterScaler =
      Property(defaultValue: 1, id: 'counterScaler');
}

Second configuration:

class SecondSettings extends SettingsModel {
  @override
  List<BaseProperty> get properties => [name];

  static const Property<String> name =
      Property(defaultValue: 'John', id: 'name');
}

2.2. Let's create models for our property groups (to locate them in the tree) and initialize them:

var firstSettings = FirstSettings();
var secondSettings = SecondSettings();

await firstSettings.init();
await secondSettings.init()

2.3. Let's implement settings through MultiSettings() and a special class for nested settings SettingsProvider():

runApp(
    MultiSettings(
      providers: [
        SettingsProvider(model: firstSettings),
        SettingsProvider(model: secondSettings),
      ],
      child: const MultiSettingsApp(),
    ),
  );

2.4 Access to settings in MultiSettings

Settings.from<FirstSettings>(context).get(counterScaler);

Settings.from<SecondSettings>(context).get(name);

// or

context.setting<FirstSettings>().get(counterScaler);

context.setting<SecondSettings>().get(name);

3. The EnumProperty() property class with EnumPropertyBuilder() widget class. Example of usage

The EnumProperty() class is inherited from the BaseProperty() class. So, it can be considered that enum properties are similar to properties. Yes, although enum property share many commonalities, they can't be processed without converting them to properties. This is the task of the EnumPropertyConverter(), which parses enums and transforms them to strings, creating mappings (Enum -> String) or (String -> Enum) for the current session of the app, and it creates a set of properties to be passed to the SettingsController().

3.1. The EnumProperty()

Let's create enum property:

EnumProperty<ThemeMode> themeMode =
    EnumProperty(
      actions: ThemeMode.values, 
      defaultValue: ThemeMode.dark
    );

In our example, we use the standard Enum from the Flutter library known as the ThemeMode.

As you can see, EnumProperty() are very similar to properties. Unlike Property(), we should pass the parameter actions, which is of type List<Enum>. To get a list of enums you should call Enum.values. But the id parameter shouldn't be passed.

3.2. Create a class based on SettingsModel with enum property

class GeneralSettings extends SettingsModel {
  
  ...

  List<BaseProperty> get properties => [themeMode];

  ...

  static const EnumProperty<ThemeMode> themeMode =
      EnumProperty(actions: ThemeMode.values, defaultValue: ThemeMode.dark);
}

The general main() function would look like this:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  var generalSettings = GeneralSettings();

  await generalSettings.init();

  runApp(
    Settings(
      model: generalSettings,
      child: const ScenarioApp(),
    ),
  );
}

3.3. Use a EnumPropertyBuilder to build a UI that depends on the Enum value

class EnumPropertyApp extends StatelessWidget {
  const EnumPropertyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EnumPropertyBuilder<ThemeMode, GeneralSettings>(
      builder: (context, value) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter Demo',
          initialRoute: '/',
          theme: ThemeData(),
          darkTheme: ThemeData.dark(),
          themeMode: value,
          home: const MyHomePage(title: 'Setting EnumPropertyBuilder Example'),
        );
      },
      property: GeneralSettings.themeMode,
    );
  }
}

Consider ScenarioBuilder<T, P>() individually:

EnumPropertyBuilder<ThemeMode, GeneralSettings>(
      builder: (context, value) {
        return MaterialApp(
          ...
          themeMode: value,
          ...
        );
      },
      property: GeneralSettings.themeMode,
    );

As we can see, using Enum to build a dependent UI is quite simple. The EnumPropertyBuilder() class has its own builder that accepts context and value. The value itself is the Enum value we need and can use. And the property parameter accepts the EnumProperty() property on which the specific EnumPropertyBuilder() depends.

4. The ConfigModel() property class with ConfigBuilder() or Config() widget class. Example of usage

This is the most automatic solution, where implementation and access to settings.

To do this, it is enough to create a class that based on ConfigModel:

class MyConfig extends ConfigModel {
  ...
}

You will need to implement get methods to obtain the necessary configuration data:

class MyConfig extends ConfigModel {

  @override
  List<ConfigPlatform> get platforms => throw UnimplementedError();

  @override
  List<BaseProperty> get properties => throw UnimplementedError();

  ...
}

Also, for convenience, you can add properties inside this class, making the fields static:

class MyConfig extends ConfigModel {

  @override
  List<ConfigPlatform> get platforms => [ConfigsPlatform.general];

  @override
  List<BaseProperty> get properties => [isDarkMode, counterScaler, name];

  ...

  static const Property<bool> isDarkMode = const Property(
    defaultValue: false,
    id: 'isDarkMode',
    isLocalStored: true,
  );

  static const Property<int> counterScaler = const Property(
    defaultValue: 1,
    id: 'counterScaler',
    isLocalStored: false,
  );

  static const Property<String> name = const Property(
    defaultValue: "John",
    id: 'name',
    isLocalStored: false,
  );
}

Now you can conveniently access the required group of settings as follows:

MyConfig.name;

Another advantage of this approach is that you can create different settings configurations with the same fields:

class MyConfig1 extends ConfigModel {

  ...

  static const Property<String> name = const Property(
    defaultValue: "John",
    id: 'name',
    isLocalStored: false,
  );
}

class MyConfig2 extends ConfigModel {

  ...

  static const Property<String> name = const Property(
    defaultValue: "Test user",
    id: 'name',
    isLocalStored: false,
  );
}

And to have access to the necessary settings as follows:

MyConfig1.name;
MyConfig2.name;

To implement the configuration, you can use one of the following widgets:

  1. Config
  2. ConfigBuilder

The Config - only allows you to inject configuration into the widget tree. The ConfigBuilder allows you to build the required UI based on the target platform.

  1. Config:
var generalConfig = GeneralConfig();
var webConfig = WebConfig();

await generalConfig.init();
await webConfig.init();

runApp(
    Config(
      providers: [
        SettingsProvider(
          model: generalConfig,
        ),
        SettingsProvider(
          model: webConfig,
        )
      ],
      child: ConfigAppForWeb(),
    ),
  );
  1. ConfigBuilder:
var generalConfig = GeneralConfig();
var webConfig = WebConfig();

await generalConfig.init();
await webConfig.init();

runApp(
    ConfigBuilder(
      providers: [
        SettingsProvider(
          model: generalConfig,
        ),
        SettingsProvider(
          model: webConfig,
        )
      ],
      builder: (context, platform) {
        if (platform == ConfigPlatform.web) {
          return const ConfigAppForWeb();
        } else {
          return const ConfigApp();
        }
      },
    ),
  );

Migration from 0.2.1 to 0.3.0

Scenarios stopped existing separately. Now these are the same properties and only one field is needed to group all the properties together. The settings management system will do everything for you. And now the Scenario are called EnumProperty. You can see an example below:

Before:

class GeneralSettings extends SettingsModel {
  @override
  List<Property> get properties => [isDarkMode, counterScaler];

  @override
  List<Scenario<Enum>>? get scenarios => [themeMode];

  static const Property<bool> isDarkMode = Property(
    defaultValue: false,
    id: 'isDarkMode',
    isLocalStored: false,
  );

  static const Property<int> counterScaler =
      Property(defaultValue: 1, id: 'counterScaler');

  static Scenario<ThemeMode> themeMode = Scenario(
      actions: ThemeMode.values,
      defaultValue: ThemeMode.dark,
      isLocalStored: true);
}

After:

class GeneralSettings extends SettingsModel {
  @override
  List<BaseProperty> get properties => [isDarkMode, counterScaler, themeMode];

  static const Property<bool> isDarkMode = Property(
    defaultValue: false,
    id: 'isDarkMode',
    isLocalStored: false,
  );

  static const Property<int> counterScaler =
      Property(defaultValue: 1, id: 'counterScaler');

  static const EnumProperty<ThemeMode> themeMode = const EnumProperty(
      id: 'themeMode',
      values: ThemeMode.values,
      defaultValue: ThemeMode.dark,
      isLocalStored: true);
}

Additional information

I will be very glad for your active participation in the discussion, support and development of this library.

Roadmap

Tasks that still need to be solved:

  • - Implementation of file local storage for settings
  • - Writing some basic tests and thoroughly analyzing the code for possible problems
  • - Support settings from .env file
  • - Support settings from json and yaml files
  • - Writing instructions for creating own data stores (for example, for network settings)
  • - Consider possible separation of settings_provider from Flutter