Managing app settings with Java Properties

Many (most?) apps need some kind of settings the user can modify, that influence on the behavior of the app across app launches. In this post, I’ll show how to implement this with java.util.Properties class in a Java / Swing game.

As an example, I will use the Snakeses game from the previous post. In this game, user can change the difficulty, game mode and light/dark mode of the UI:

The selections will be saved to and read from a file named snakeses.ini:

#Snakeses Settings
#Tue May 06 19:28:32 EEST 2025
difficulty=hard
mode=dark
playername=Antti
style=classic

The settings file contains key-value pairs (e.g. on line difficulty=hard the difficulty is a key, and hard is the value). Managing the settings is then just handling these key-value pairs, providing a user a way to change these in a controlled manner, and then handling the saving and restoring the values with the settings file. The file can also contain commented lines (beginning with #) that are just for humans to read, and are not actual settings.

As you can see, also the latest player’s name, who entered the Hall of Fame, is also stored in the settings. Assuming the game is mostly played by the same player, she does not need to write her name again and again, but can just accept the notification as you can see in the previous post video.

The settings are managed in a single class, unsurprisingly named Settings:

public class Settings {
    public Settings() {
        difficulty = "easy";
        style = "modern";
        mode = "dark";
        playerName = "Anonymous";
    }

    public String difficulty;
    public String style;
    public String mode;
    public String playerName;

    public boolean isDirty = false;
    private static final String SETTINGS_FILE_NAME = "snakeses.ini";

As you can see, the constructor gives the default values to the various settings. You can also see that I’ve taken the easy road of letting the members be public instead of the default and recommended private. Lazy me, but I am myself making sure that as the only programmer in this project I will treat these public members responsibly and not mess the values with anything that is invalid. In a larger project I would let them be private and make sure that setter methods would check that the parameters to change the setting values would always be valid.

How to read the settings from the snakeses.ini file, is shown here:

public void read() throws IOException {
    File configFile = new File(SETTINGS_FILE_NAME);
    Properties config = new Properties();
    try (FileInputStream istream = new FileInputStream(configFile)) {
        config.load(istream);
        if (config.containsKey("difficulty")) {
            difficulty = config.getProperty("difficulty");
        } else {
            difficulty = "easy";
        }
        if (config.containsKey("style")) {
            style = config.getProperty("style");
        } else {
            style = "modern";
        }
        if (config.containsKey("mode")) {
            mode = config.getProperty("mode");
        } else {
            mode = "dark";
        }
        if (config.containsKey("playername")) {
            playerName = config.getProperty("playername");
        }
    } catch (FileNotFoundException e) {
        difficulty = "easy";
        style = "modern";
        mode = "dark";
        playerName = "Anonymous";
        save();
    } finally {
        isDirty = false;
    }
}

Opening and reading the settings file is handled using java.io.File, java.io.FileInputStream and java.util.Properties classes. The Properties object will then contain the settings read from the ini file, after calling config.load(istream).

After that, it is simple to read the setting values from the properties object by calling config.getProperty, giving the name of the property. Just to be sure, the code checks if the properties contain that key (using config.containsKey, and if it does not, a default value is used. This is necessary, since the file could be changed outside of the app, by the user, for example, so as a programmer you cannot trust that the file (after saving it) will surely contain those key-value pairs.

Also, when the app launches for the first time, the settings file is not there. It could be delivered with the app with default values, but again, nothing stops the user accidentally or deliberately deleting that file. So the code must prepare for the situation that the settings file does not exist — that’s why the catch (FileNotFoundException e). If the file is not there, again, resort to the default setting values. And then save the settings immediately, calling save(). That makes sure that at least now we have the settings file there.

The member variable isDirty is also set to false. isDirty is convenient to have. When the user opens up the settings panel and closes it, there is no sense in saving the settings if they were not changed. As you can see below, the isDirty is set to true if the settings change, and only then the settings are actually saved. No need to do any disk access if there is no need for it.

Do note that the app in this demo does not check if the settings are actually different from the original when user leaves the settings panel. If there are lots of settings and changing them leads to lots of code to be executed, you might want to keep the old settings somewhere, let the user to manipulate the settings, and then when the user is done, actually compare the old setting values to new ones. For those values that are actually different, then handle the changes to those settings only.

How about saving the settings:

public void save() throws FileNotFoundException, IOException {
    File configFile = new File(SETTINGS_FILE_NAME);
    Properties config = new Properties();
    try(FileOutputStream ostream = new FileOutputStream(configFile)) {
        config.setProperty("difficulty", difficulty);
        config.setProperty("style", style);
        config.setProperty("mode", mode);
        config.setProperty("playername", playerName);
        config.store(ostream, "Snakeses Settings");
    } finally {
        isDirty = false;
    }
}

Saving settings to a file is handled using java.io.File, java.io.FileOutputStream and java.util.Properties classes. In this case we just set the various properties of the Properties object, using config.setProperty, giving the key and value pairs to the configuration, and finally call config.store. As you can see, the string “Snakeses Settings” and the date of the update is saved as comments to the snakeses.ini file.

Also, the isDirty is set to false at this time; settings are now saved and not changed.

How the Settings panel then uses the settings when user is shown the current settings? As an example, let’s see how the game difficulty radio buttons are created and how the radio button corresponding to the current setting is selected, in SettingsPanel constructor, other details omitted:

public class SettingsPanel extends JPanel implements ActionListener {

    private Settings settings;

    public SettingsPanel(Settings settings) {
        this.settings = settings;

        ButtonGroup levelGroup = new ButtonGroup();
        JRadioButton easy = new JRadioButton("Easy");
        easy.setActionCommand("easy");
        easy.addActionListener(this);
        JRadioButton hard = new JRadioButton("Hard");
        hard.setActionCommand("hard");
        hard.addActionListener(this);
        levelGroup.add(easy);
        levelGroup.add(hard);
        if (settings.difficulty.equals("easy")) {
            easy.setSelected(true);
        } else {
            hard.setSelected(true);
        }

The “Easy” and “Hard” radio buttons are created to a ButtonGroup. That takes care of managing the principle of the related radiobuttos that only one of them can be selected at one time. The last if-else block shows how the current value of the Settings is used to select one or the other from these game difficulty radio buttons.

Did you know that the name “radio button” for this control comes from the actual radios to listen to radio stations? Radios used to have several buttons for selecting preselected radio station frequencies to listen to. Obviously, you can only listen to one station at a time. So pressing down one station button would then deselect (pop up) the previously selected station button.

That’s why these buttons are called “radio buttons”. Radio buttons in GUI frameworks are round to distinguish them from check boxes, which are rectangular and function differently (many of them can be selected simultaneously).

A vintage radio device with white radio buttons and two round controls to change the volume and control the frequency.
Row of seven white rectangular radio buttons in a vintage radio divide. Image from https://www.collectorsweekly.com/stories/273171-1950s-grundig-fleetwood-tube-radio-mode

Obviously, the controls to use in managing the settings depends on the setting and the possible values of that setting. For example, if we would have a setting with multiple values, like e.g. 42 distinct values, using radio buttons would not be a good choice. Then one could use a dropdown / combobox list instead, populating the available options to the list and selecting the one found in the settings.

What happens then when user changes the setting? In the code above, you can see that the SettingsPanel is set as the action listener to the radiobuttons, e.g. in easy.addActionListener(this). Also, each radio button were given a name for the command they represent, e.g. like this: easy.setActionCommand("easy").

The panel implements the ActionListener interface, and therefore must have the method actionPerformed overridden. Again, let’s look at the relevant parts of this method:

public void actionPerformed(ActionEvent e) {
    final String action = e.getActionCommand();
    if (action.equals("easy") || action.equals("hard")) {
        if (!settings.difficulty.equals(action)) {
            settings.difficulty = action;
            settings.isDirty = true;
        }
    } else if (action.equals("classic") || action.equals("modern")) {
// and the same for the other radio buttons...

Here, we check which was the action command related to the action event. So if the command was “easy” or “hard”, we check if the related setting value is different, we then change it and also set the isDirty of the setting to true indicating a need to actually save the settings to the snakeses.ini file as seen above.

So far we have seen how the Settings class manages the settings file, and how the SettingsPanel displays the current settings, and then changes the settings based on user actions.

How the app then actually initializes the settings and saves them if they have been changed? This is done, in this app, in the SnakesesApp class that contains the main method of the app.

When the app is launched, the settings object is the first one to be created:

    public static void main( String[] args )
    {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new SnakesesApp().run();
            }
        });        
    }
    private static SnakesesGame game;
    private static Settings settings;
    private static HallOfFame hallOfFame;

// Later...

private void run() {
    try {
        settings = new Settings();
        settings.read();
        hallOfFame = new HallOfFame();
        hallOfFame.read();
        game = new SnakesesGame(GameViewConstants.GAME_WIDTH, GameViewConstants.GAME_HEIGHT, settings);
        // GUI
        JFrame mainFrame = new JFrame("Snakeses");
// And the rest of the GUI is then created...

Things to note here: The app object contains the relevant application objects as member variables (game, settings, hall of fame). Also the GUI objects (frame, panels) are member variables, but left out from code snippet above since they are not relevant in this post.

As you can see, the settings are read from the settings file as the very first step of the application launch. Therefore, they are read from the file and available to the rest of the application immediately.

How about saving the settings? This could have been done in the SettingsPanel, but in this implementation it is done also in the SnakesesApp. The reason is, that changing some of the settings needs to be reflected in other components of the app. The method SnakesesApp.switchTo is called by the SettingsPanel when the close button is pressed. The app then switches back to the game view.

But before that, the app first checks if the settings were changed (isDirty is true), and saves the settings. App also instructs the hall of fame object to switch the hall of fame visible to the user, to the corresponding game difficulty level hall of fame data. This is because the game always shows the hall of fame for the current difficulty level only. This is shown in the code snippet below:

public static void switchTo(final View view) {
    CardLayout cardLayout = (CardLayout) (mainView.getLayout());
    if (settings.isDirty) {
        try {
            settings.save();                
            hallOfFame.setCurrentLevel(
                HallOfFame.Level.fromString(settings.difficulty)
            );
// ...

Other game objects and panels read the settings continuously as they do their things, so they need not to be updated the same way as the hall of fame needs to be. For example, the game panel that draws the snake and the food, uses the light/dark mode setting in painting the graphics each time the game screen is drawn:

public void paintComponent(Graphics g) {
	super.paintComponent(g);
	if (settings.mode.equals("dark") && getBackground().equals(Color.WHITE)) {
		setBackground(Color.BLACK);
	} else if (settings.mode.equals("light") && getBackground().equals(Color.BLACK)) {
		setBackground(Color.WHITE);
	}
// ....

Summarizing, we have now seen:

  • how the game settings and the settings file can be managed using the Java Property class,
  • how game settings panel can display and enable changing the settings,
  • how the app initializes the settings at lauch and how it saves the settings if they are changed in the settings panel,
  • how settings are used in the app when drawing the game screen, and
  • how changed settings can be propagated, if necessary, to other game elements like the hall of fame.