• Blog
  • Storing cross-platform application settings in JSON

Storing cross-platform application settings in JSON

Storing cross-platform application settings in JSON

Publish date: 29.06.2022

    DISCOVER MORE OF WHAT MATTERS TO YOU

    Storing cross-platform application settings in JSON

    Several approaches to storing data related to program settings have been replaced by newer ones since the introduction of Delphi. In general, they always followed the principles of a mother operating system.

    Initially, INI files were widely used, then the use of a register was rather popular and then it became common to store data in files again. But due to some challenges, it was decided to use XML files instead of INI and then they were replaced with JSON.

    Let’s have a look at the issues that a developer can face during the realization of settings storage. The methods that we will analyze in this article are not the only ones that are correct. But they have been already applied for several projects and have proved their efficiency in practice.

    Looking ahead, we should note that the key peculiarity and the most pleasant part of the method is its automated serialization and deserialization of JSON objects in records and Delphi classes.

    Modern Delphi versions offer two libraries for working with JSON. In this article, we will analyze the most widely used alternative open-source variant – X-SuperObject provided by a Turkish developer Onur YILDIZ 

    https://github.com/onryldz/x-superobject

    Let’s start. In the folder with our original program texts, let’s create a folder Subrepos and execute there

    git clone https://github.com/onryldz/x-superobject.git 

    We will see a folder x-superobject. Do not forget to add it to the Project — Options — Delphi Compiler — Search path “./subrepos/x-superobject”. 

    Initial realization

    For storing the MyProg program settings, let’s create a module MP.Settings and a class inside it

    123456789101112
    unit MP.Settings;
    uses
    XsuperObject;
    TMyProgSettings = class()
    public
    end;
    var
    Settings : TMyProgSettings;

    We have to choose where we will store a settings file. There can be a lot of variants, we will consider them later.

    Now let’s make a simple decision and return to it later to make refactoring.

    We are developing software for Windows and a commonly used platform today is Windows 64-bit. The development process is going on in an unprotected folder. That’s why we will store a settings file directly close to an exe file.

    To ensure minimum refactoring, let’s introduce special methods for the class

    123456789
    class function TMyProgSettings.GetDefaultSettingsFilename: string;
    begin
    Result := TPath.Combine(GetSettingsFolder(), 'init.json');
    end;
    class function TMyProgSettings.GetSettingsFolder: string;
    begin
    Result := ExtractFilePath(ParamStr(0));
    end;

    Now let’s turn to the main form of the app.

    Simple main form to display and change some values stored in the settings file

    Thereby, let’s add fields to the settings class

    1234
    public
    Chk1 : boolean;
    Chk2 : boolean;
    RadioIndex : integer;

    A constructor to indicate values of settings by default

    constructor TMyProgSettings.Create;

    12345
    begin
    Chk1 := True;
    Chk2 := False;
    RadioIndex := 0;
    end;

    And the main content – methods of saving/loading data from a file.
    Pay attention to the following moments:

    By default the settings know in what file they are placed, a file name is an optional thing

    serialization/deserialization take place in one line with helpers from XSuperObject

    123456789101112131415161718192021222324
    procedure TMyProgSettings.LoadFromFile(AFileName: string = '');
    var
    Json : string;
    begin
    if AFileName = '' then
    AFileName := GetDefaultSettingsFilename();
    if not FileExists(AFileName) then
    exit;
    Json := TFile.ReadAllText(AFileName, TEncoding.UTF8);
    AssignFromJSON(Json); // magic method from XSuperObject's helper
    end;
    procedure TMyProgSettings.SaveToFile(AFileName: string = '');
    var
    Json : string;
    begin
    if AFileName = '' then
    AFileName := GetDefaultSettingsFilename();
    Json := AsJSON(True); // magic method from XSuperObject's helper too
    TFile.WriteAllText(AFileName, Json, TEncoding.UTF8);
    end;

    In the main form, the TMainForm.LoadSettings method will be responsible for settings loading. If a file doesn’t exist, it will be created and the default settings will be used.

    Such methods as LoadFromSettings and SaveToSettings are responsible for transferring settings to the interface elements and vice versa.

    1234567891011121314151617181920212223242526272829303132333435363738
    procedure TMainForm.LoadSettings;
    begin
    if Settings = nil then
    Settings := TMyProgSettings.Create;
    if not FileExists(Settings.GetDefaultSettingsFilename()) then
    begin
    ForceDirectories(Settings.GetSettingsFolder());
    Settings.SaveToFile();
    end;
    Settings.LoadFromFile();
    LoadFromSettings();
    end;
    // load UI components from settings
    procedure TMainForm.LoadFromSettings();
    begin
    chk1.IsChecked := Settings.Chk1;
    chk2.IsChecked := Settings.Chk2;
    rb1.IsChecked := Settings.RadioIndex = 0;
    rb2.IsChecked := Settings.RadioIndex = 1;
    rb3.IsChecked := Settings.RadioIndex = 2;
    end;
    // Save UI components state to settings
    procedure TMainForm.SaveToSettings();
    begin
    Settings.Chk1 := chk1.IsChecked;
    Settings.Chk2 := chk2.IsChecked;
    if rb1.IsChecked then
    Settings.RadioIndex := 0
    else if rb2.IsChecked then
    Settings.RadioIndex := 1
    else if rb3.IsChecked then
    Settings.RadioIndex := 2;
    end;

    Now we need to call the LoadSettings method in the constructor of the main form and start the first variant of our program.

    Form displays values read from settings

    As we can see, the default values were applied, the interface is filled in according to the values from Settings.

    Let’s have a look at the settings file. It is placed near the exe file and is named init.json.

    12345
    {
    "chk1":true,
    "chk2":false,
    "radioIndex": 0
    }

    If you call SaveToSettings() after changing every component and save Settings.SaveToFile()  settings every time you close the program, the file content will change in accordance with your actions. 

    Contained objects

    Let’s make our project a little bit closer to reality.

    For opening a program, a user will have to enter a username and a password. Moreover, to make the interface more user-friendly, we will save the recent username.

    Let’s create the simplest login form.

    Login form

    To the constructor of the main form,we will add a call for the following method

    1234567891011121314151617181920212223
    procedure TMainForm.TryLogin();
    var
    F: TLoginForm;
    Login, Password: string;
    begin
    F := TLoginForm.Create(NIL);
    try
    while not Application.Terminated do
    begin
    if F.ShowModal = mrOK then
    begin
    Login := F.edtLogin.Text;
    Password := F.edtPassword.Text;
    if LoginCorrect(Login, Password) then
    Break;
    end
    else
    Application.Terminate;
    end;
    finally
    F.Free;
    end;
    end;

    Pay attention to the fact that we are talking about a desktop platform where we have modal forms. For mobile apps, a login form will be supported in a slightly different way. But now we have another topic for consideration.

    Let’s start a program and make sure that everything is working correctly. In the LoginCorrect method, we will write the right username and password and later it will be possible to conduct real testing.

    Now let’s come back to the settings. In order to make our example more complicated, we won’t just create a new field in the settings but make it of a more complicated type, a record, and will store there a user’s choice on whether we need to show the recent login.

    1234567891011121314151617181920212223242526272829
    TLoginSettings = record
    UserName: string;
    ShowRecent: Boolean;
    end;
    TMyProgSettings = class
    public
    Chk1: Boolean;
    Chk2: Boolean;
    RadioIndex: integer;
    Login: TLoginSettings;
    Let’s add interface loading and saving methods to the login form.
    procedure TLoginForm.LoadFromSettings;
    begin
    edtLogin.Text := Settings.Login.UserName;
    chkShowRecentUsername.IsChecked := Settings.Login.ShowRecent;
    end;
    procedure TLoginForm.SaveToSettings;
    begin
    if chkShowRecentUsername.IsChecked then
    Settings.Login.UserName := EditLogin.Text
    else
    Settings.Login.UserName := '';
    Settings.Login.ShowRecent := chkShowRecentUsername.IsChecked;
    end;

    And add method calls into a constructor and destructor, respectively.

    We won’t need to change anything in the settings storage and saving and loading methods. Everything is working. Let’s check. Here you can see the saved settings file.

    12345678910
    {
    "chk1":false,
    "chk2":true,
    "radioIndex": 2,
    "login":
    {
    "username":"admin",
    "showRecent":true
    }
    }

    Here’s what we will see during the repeated loading.

    Let’s ensure that stored value is used

    The program is working.

    Details and cross-platform differences

    At the start of realization, we’ve decided to store our settings file near the exe file. If our program is installed via an installer, it is highly possible that it will be placed somewhere inside the Project Files folder.

    Access to the record of data there is prohibited by default, that’s why it is not a good idea to store there a settings file.

    For these aims, we have the following folder C:\Users\<username>\AppData\Roaming . 

    It will be offered by the System.IOUtils.TPath.GetHomePath method.

    Let’s change the TMyProgSettings.GetSettingsFolder method:

    1234
    class function TMyProgSettings.GetSettingsFolder: string;
    begin
    Result := TPath.Combine(TPath.GetHomePath(), 'MyProg');
    end;

    Now on all platforms, the file will be saved correctly. On iOS and Android – in its own app data storage that is protected from other programs, on Windows – in the user’s data folder. On MacOC – in the user catalog /Users/<username>  but it can be not very convenient that’s why it is possible to offer an alternative variant – for example, in the folder Library. On Windows, GetLibraryPath points to the app folder.

    12345678
    class function TMyProgSettings.GetSettingsFolder: string;
    begin
    {$IFDEF MACOS}
    Result := TPath.Combine(TPath.GetLibraryPath(), 'MyProg');
    {$ELSE}
    Result := TPath.Combine(TPath.GetHomePath(), 'MyProg');
    {$ENDIF}
    end;

    As at the first start of the program, we didn’t have this folder, it is necessary to add to the first settings saving the call of

    1
    ForceDirectories(Settings.GetSettingsFolder());

    Let’s summarize what we have:

    Pluses of this method

    The described method allows adding data of a complex structure that are saved between the program starts without additional programming. Of course, it is necessary to write code where the data is used. But the code is simple and it is easy to support it.

    Changes in a class structure automatically lead to changes in a file structure. 

    It is important that if some fields are not presented in a file or, vice versa, there are some extra fields, it doesn’t lead to errors in processing.

    The JSON format is easy to read and it is simple to introduce changes to a saved file at the stage of debugging.

    Minuses of the method

    JSON is not intended for storing huge data arrays

    It is not comfortable to store binary data, for example, uploaded images

    On desktop platforms, a settings file is not protected from unauthorized viewing and updating

    In general, all the mentioned problems have a solution that’s why the method considered in this article can be a very good choice.

    https://github.com/SoftacomCompany/FMX.Settings-in-JSON

    Subscribe to our newsletter and get amazing content right in your inbox.

    This field is required
    This field is required Invalid email address

    Thank you for subscribing!
    See you soon... in your inbox!

    confirm your subscription, make sure to check your promotions/spam folder

    Subscribe to our newsletter and get amazing content right in your inbox.

    You can unsubscribe from the newsletter at any time

    This field is required
    This field is required Invalid email address

    You're almost there...

    A confirmation was sent to your email

    confirm your subscription, make sure to check
    your promotions/spam folder