Android SharedPreferences Tutorial and Example

Reading and Writing Complex Objects with SharedPreferences and Gson

SharedPreferences is convenient for small Android settings, such as a dark-mode flag, selected language or notification preference. It stores primitive values, so saving a complete user profile, shopping basket or nested application state requires an additional serialisation step.

Gson converts Java or Kotlin objects into JSON and reconstructs them later. Together, Gson and SharedPreferences provide a lightweight solution for modest data sets, such as a café loyalty profile in Melbourne or a saved shopping basket for an Australian online store. This approach is simple, although it is not a replacement for SQLite when records become large or require searching.

Storage approach Best suited to Main benefit Main limitation
Primitive SharedPreferences values Flags, strings and numbers Very little code Cannot represent object structure directly
Gson with SharedPreferences Small profiles, settings and baskets Easy object serialisation Entire object is read and written as one JSON value
SQLite or Room Collections and searchable records Queries, indexing and scalable data More setup and database management
Files or cloud storage Large documents and synchronised data Handles larger payloads Requires file or network error handling

Why object serialisation is useful

A complex object may contain strings, numbers, Boolean values, lists and other objects. For example, a retail application might store a customer with an Australian postcode, preferred store in Brisbane and a list of saved products. Saving every property separately creates repeated keys and makes future model changes harder to manage.

Gson solves this by converting the object into a JSON string. The JSON is stored under one preference key, then converted back into the original class when the application needs it. The pattern is commonly called serialisation when writing and deserialisation when reading.

SharedPreferences is suitable when the data is small and local. It works well for a session snapshot or a few user settings, but storing hundreds of products or a complete transaction history can produce slow reads and oversized preference files. Use Room or SQLite for data that needs filtering, sorting or frequent updates.

Add Gson and define a model

For a Java Android project, add Gson to the module-level Gradle file:

dependencies {
    implementation 'com.google.code.gson:gson:2.10.1'
}

A simple model can contain ordinary fields and a nested list. Keep a no-argument constructor available if your project or tooling expects one:

public class UserProfile {
    private String displayName;
    private String email;
    private String postcode;
    private boolean notificationsEnabled;
    private List<Store> favouriteStores;

    public UserProfile() {
    }

    // Add getters and setters as required
}

The nested Store class might look like this:

public class Store {
    private String name;
    private String suburb;

    public Store() {
    }

    public Store(String name, String suburb) {
        this.name = name;
        this.suburb = suburb;
    }
}

A postcode should generally be kept as a String, not an integer. Australian postcodes can begin with zero, so converting "0800" to an integer would lose important information. The same principle applies to customer identifiers and phone numbers.

Write an object to SharedPreferences

Create a Gson instance, convert the object with toJson() and save the resulting string. The preference file name and key should be stable constants so that every part of the application uses the same location:

private static final String PREFS_NAME = "app_preferences";
private static final String PROFILE_KEY = "user_profile";

public void saveProfile(Context context, UserProfile profile) {
    SharedPreferences preferences =
            context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);

    Gson gson = new Gson();
    String json = gson.toJson(profile);

    preferences.edit()
            .putString(PROFILE_KEY, json)
            .apply();
}

apply() updates the in-memory value immediately and writes it to disk asynchronously. It is usually appropriate for normal UI actions, such as saving a preference after a user selects a Sydney store. commit() writes synchronously and returns a success value, but it can block the main thread, so it should be used carefully.

Avoid saving sensitive information in plain SharedPreferences. JSON stored this way can be inspected on a rooted device or through debugging tools. Passwords, payment details and authentication tokens need a safer approach, such as encrypted storage and a server-side token policy. Australian applications should also consider the Privacy Act and the Australian Privacy Principles when deciding what personal information is retained locally.

Read the object safely

Reading involves retrieving the JSON string and passing it to fromJson(). Always handle a missing value because the user may be opening the application for the first time:

public UserProfile loadProfile(Context context) {
    SharedPreferences preferences =
            context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);

    String json = preferences.getString(PROFILE_KEY, null);

    if (json == null || json.isEmpty()) {
        return null;
    }

    try {
        return new Gson().fromJson(json, UserProfile.class);
    } catch (JsonSyntaxException exception) {
        return null;
    }
}

A malformed value can occur after a model change, an interrupted write or manual test data being inserted. Returning null is acceptable for a small example, although a production application may log the problem, remove the damaged value and create a default profile. Do not expose the raw JSON or personal data in release logs.

Gson fills fields it recognises and normally leaves missing fields at their Java defaults. This helps when adding a field such as notificationsEnabled in a later version. For more significant migrations, add a version field to the stored model or use a dedicated migration strategy rather than relying on accidental defaults.

Handle lists and nested objects

Gson can serialise lists without a separate adapter when the list contains ordinary model classes:

UserProfile profile = new UserProfile();
// Set profile fields and favourite stores here

String json = new Gson().toJson(profile);

When reading a list directly, Java’s generic type information must be supplied with TypeToken. Without it, Gson cannot reliably determine the list’s element type:

Type listType = new TypeToken<List<Store>>() {}.getType();
List<Store> stores = new Gson().fromJson(json, listType);

For a complete UserProfile, fromJson(json, UserProfile.class) is enough because Gson can inspect the fields declared by that class. Dates, currency types and custom formats may need additional configuration. For example, an Australian checkout app should define how it stores an order timestamp and represent Australian dollar amounts carefully, preferably as integer cents rather than floating-point values.

If a field name in JSON differs from the Java field name, use @SerializedName:

@SerializedName("preferred_store")
private String preferredStore;

This is useful when an existing API uses snake case while the Android model follows camel case. It also gives the stored format a stable name if the code field is later renamed.

Choose sensible storage boundaries

A preference repository keeps storage logic out of an Activity or Fragment. This makes the code easier to test and prevents different screens from accidentally using different keys:

public class ProfileRepository {
    private final SharedPreferences preferences;
    private final Gson gson = new Gson();

    public ProfileRepository(Context context) {
        preferences = context.getSharedPreferences(
                "app_preferences", Context.MODE_PRIVATE);
    }

    public void save(UserProfile profile) {
        preferences.edit()
                .putString("user_profile", gson.toJson(profile))
                .apply();
    }

    public UserProfile get() {
        String json = preferences.getString("user_profile", null);
        return json == null ? null : gson.fromJson(json, UserProfile.class);
    }
}

Keep the stored object focused. A loyalty application serving customers in Adelaide, Perth or the Gold Coast may need to preserve display preferences and a selected store, while server-owned balances and order statuses should be refreshed from the backend. Local JSON should be treated as a cache or convenience copy, not as the authoritative source for business-critical data.

Test the complete cycle: create an object, save it, recreate the repository, read it and compare its fields. Also test an empty preference file, invalid JSON, a missing nested list and a model containing an Australian postcode such as "0800". These cases reveal data-loss bugs before the application reaches users on different Android versions.

Know when to use another database

Gson with SharedPreferences is effective for one small object or a small group of related settings. It is especially practical in tutorials, prototypes and simple utilities where the entire value can be loaded at once. The implementation is short, readable and easy to adapt to Java Android projects.

The approach becomes less suitable when users can save many products, when individual records change frequently or when the application needs queries such as “show all orders from Melbourne”. Rewriting a large JSON document for every small change increases work and can create conflicts between concurrent updates.

Room, backed by SQLite, is a better choice for relational data, searchable collections and offline-first applications. DataStore is another modern option for preferences, particularly in Kotlin projects, although complex object serialisation still requires a defined format. Selecting storage based on data size, sensitivity and access patterns keeps the Android application reliable as its local market and feature set grow.