• Blog
  • Storing cross-platform application settings in JSON

Storing cross-platform application settings in JSON

Method of storing application settings in JSON files

Publish date:
Discover more of what matters to you

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

Tags

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