Skip to main content

Command Palette

Search for a command to run...

How to Implement DataStore in Kotlin Applications

Updated
โ€ข7 min read
How to Implement DataStore in Kotlin Applications
P

An insatiably curious mind, I love exploring, assembling, and seasoning my ideas, whether in code or in the kitchen.

Hi Kooktliners ๐Ÿ‘‹

You're probably almost finished with your app and need something to store your user settings. DataStore is perfect for that !

Jetpack DataStore is a modern data storage solution that lets you store user settings or typed objects. It uses Coroutines and Flow to store data:

  • Asynchronously: Read and write operations are done in the background, off the main thread, so there's no risk of ANR.

  • Consistent: Ensures that your data remains accurate, even in case of failures or interruptions.

  • Transactionally: When multiple values are changed, all changes are applied together, or none are. This prevents partial updates and inconsistent states.

Pretty cool, right? ๐Ÿ˜Ž
DataStore comes in two types:

  • Preferences DataStore: Stores simple key-value pairs without a predefined schema. Note: it doesn't offer type safety.

  • Proto DataStore: Allows you to persist typed objects but requires a predefined schema.

Alright, enough theory, letโ€™s Koockt! ๐Ÿ‘จโ€๐Ÿณ

Preference Datastore

class PreferenceDSManager ( private val context: Context) {

    private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name ="settings")
    suspend fun <T> savePreference(key: Preferences.Key<T>, value:T) {
        context.dataStore.edit { prefs->
            prefs[key] = value
        }
    }

    fun <T> getPreference(key: Preferences.Key<T>): Flow<T?> {
        return  context.dataStore.data.map { prefs-> prefs[key] }

    }

    suspend fun <T> getPreferenceValue(key: Preferences.Key<T>, default: T) : T {
        return getPreference(key).first() ?:default
    }

}


object Keys {
    val KEY_BOOLEAN = booleanPreferencesKey("key_boolean")
    val KEY_STRING = stringPreferencesKey("key_string")
    val KEY_INT = intPreferencesKey("key_int")
}

Proto DataStore

You need 3 ingredients:

1. A schema

Create a .proto file in the app/src/main/proto/ directory. It defines the type of your persisted variables.
(See the Protocol Buffers language guide for details.)

syntax = "proto3";

option java_package = "com.codelog.datastorerecipe.datastore";
option java_multiple_files = true;

//User preferences
message Userpreferences {

  // Hour format
 bool boolPref = 1;

 string strField = 2;

 int32 numField = 3;

  // Contrast level
  enum ContrastLevel {
    MIN = 0;
    MEDIUM = 1;
    HARD  = 2;
  }

  ContrastLevel contrast_level = 4;

}

2. A Serializer

It's like a translator , it basically converts your Kotlin data into a format that matches your schema file.

/**
 * Serializer implementation for `Userpreferences` protocol buffer messages.
 *
 * This object provides methods to serialize and deserialize `Userpreferences` data
 * for use with Jetpack's DataStore. It ensures that all protocol buffer data
 * is properly handled, including setting a default value and managing error scenarios
 * when parsing from an input stream.
 *
 * The `defaultValue` property provides a way to define the initial default proto state,
 * while the serialization and deserialization logic is handled by overriding
 * the `readFrom` and `writeTo` methods.
 */
object PreferenceProtoSerializer : Serializer<Userpreferences> {

    /**
     * Provides the default value for the preferences model `Userpreferences`.
     *
     * This property returns an instance of `Userpreferences` that represents the default
     * state as defined by the `getDefaultInstance` method. It is typically used as the
     * initial or fallback value when no specific preference data has been set or when
     * retrieving the default state of the preferences object in the absence of user-defined data.
     *
     * The returned instance serves as a baseline for the preference data and ensures
     * consistency across the application whenever the default configuration is required.
     */
    override val defaultValue: Userpreferences
        get() = Userpreferences.getDefaultInstance()

    /**
     * Reads and deserializes a Userpreferences object from the provided InputStream.
     *
     * This method parses the InputStream to create an instance of Userpreferences using the
     * `parseFrom` method. If the InputStream cannot be parsed, an InvalidProtocolBufferException
     * is thrown to indicate the error.
     *
     * @param input The InputStream containing the serialized data to be read and parsed.
     * @return A Userpreferences instance deserialized from the provided InputStream.
     * @throws InvalidProtocolBufferException If the InputStream cannot be parsed into a Userpreferences object.
     */
    override suspend fun readFrom(input: InputStream): Userpreferences {
        try {
            return Userpreferences.parseFrom(input)
        } catch (e: InvalidProtocolBufferException) {
            throw InvalidProtocolBufferException("Cannot read proto.", e)
        }
    }

    /**
     * Writes the provided `UserPreferences` object to the specified `OutputStream`.
     *
     * @param t The `UserPreferences` instance to be written to the output stream.
     * @param output The `OutputStream` to which the `UserPreferences` instance will be serialized.
     */
    override suspend fun writeTo(
        t: Userpreferences,
        output: OutputStream
    ) {
        t.writeTo(output)
    }

}

3. And finally your class manager :

/**
 * A manager class for handling user preferences using Proto DataStore.
 *
 * This class provides functionality to save and retrieve strongly-typed user preferences
 * such as boolean, string, integer fields, and enums. The user preferences are stored
 * in a Proto DataStore file named "user_prefs.pb", utilizing a protobuf-based serialization mechanism.
 *
 * It is designed to efficiently manage user-specific customization settings persistently while providing
 * an interface for easy and straightforward operations.
 *
 * @constructor Initializes the manager with the given application context.
 * @param context The application context used for accessing the DataStore.
 */

class PreferenceProtoDSManager  ( private val context: Context) {

    /**
     * Extension property providing a `DataStore` instance configured to work with protobuf-based user preferences.
     * This property is scoped to a `Context` and utilizes the `PreferenceProtoSerializer` for serialization.
     *
     * The `userPreferencesDataStore` is used to manage persistent storage and retrieval of user-specific
     * preferences, including various fields such as boolean, string, integer, and enum types. The preferences
     * are stored in a file named "user_prefs.pb".
     *
     * - To read data, use the associated `data` property on the `DataStore` instance, which provides a flow of
     *   the `Userpreferences` object.
     * - To update data, use the `updateData` function on the `DataStore` instance with the current builder
     *   instance of `Userpreferences`.
     *
     * Example purposes include storing user customization settings, such as theme preferences or contrast levels.
     *
     * Errors during data reading are handled by emitting the default instance of `Userpreferences`.
     */
    private val Context.userPreferencesDataStore: DataStore<Userpreferences> by dataStore(
        fileName = "user_prefs.pb",
        serializer = PreferenceProtoSerializer
    )

    /**
     * A Flow that emits the user preferences data stored in the `userPreferencesDataStore`.
     *
     * This flow listens to changes in the underlying Proto DataStore and emits updated
     * `Userpreferences` objects whenever the data changes. In case of an exception during
     * data retrieval, it emits the default `Userpreferences` instance and logs the exception.
     *
     * The default instance of `Userpreferences` is provided by the `getDefaultInstance()` method.
     * Exceptions are caught and logged using `Log.e` to ensure seamless operations and handling
     * of fallback scenarios.
     */
    val userPreferencesFlow: Flow<Userpreferences> =context.userPreferencesDataStore.data
        .catch {  exception ->

            emit(Userpreferences.getDefaultInstance())
            Log.e ("Error"," PreferenceProtoDSManager::userPreferencesFlow message "+exception.message)
        }


    /**
     * Saves the given boolean preference value to the UserPreferences DataStore.
     *
     * @param boolPref The boolean value to be stored in the preferences.
     */
    suspend fun saveBoolPref(boolPref: Boolean) {
        context.userPreferencesDataStore.updateData {
            it.toBuilder().setBoolPref(boolPref).build()
        }
    }

    /**
     * Retrieves the boolean preference value from the UserPreferences DataStore.
     *
     * @return The boolean value stored in the preferences.
     */
    suspend fun getBoolPref(): Boolean {
        return context.userPreferencesDataStore.data.first().boolPref
    }

    /**
     * Saves the given string field value to the UserPreferences DataStore.
     *
     * @param strField The string value to be stored in the preferences.
     */
    suspend fun saveStrField(strField:String) {
        context.userPreferencesDataStore.updateData {
            it.toBuilder().setStrField(strField).build()
        }
    }

    /**
     * Retrieves the string field value from the UserPreferences DataStore.
     *
     * @return The string value stored in the preferences.
     */
    suspend fun getStrField() : String {
        return context.userPreferencesDataStore.data.first().strField
    }

    /**
     * Saves the given integer field value to the UserPreferences DataStore.
     *
     * @param numField The integer value to be stored in the preferences.
     */
    suspend fun saveNumField(numField:Int) {
        context.userPreferencesDataStore.updateData {
            it.toBuilder().setNumField(numField).build()
        }
    }

    /**
     * Retrieves the integer value of the numField from the UserPreferences DataStore.
     *
     * @return The integer value stored in the numField preference.
     */
    suspend fun getNumField() : Int {
        return context.userPreferencesDataStore.data.first().numField
    }

    /**
     * Saves the specified contrast level value to the UserPreferences DataStore.
     *
     * @param contrastLevel The contrast level value to be stored in the preferences.
     */
    suspend fun saveContrastLevel(contrastLevel: Userpreferences.ContrastLevel) {
        context.userPreferencesDataStore.updateData {
            it.toBuilder().setContrastLevel(contrastLevel).build()

        }
    }

    /**
     * Retrieves the contrast level preference value from the UserPreferences DataStore.
     * This function suspends while it fetches the data.
     *
     * @return The contrast level value stored in the UserPreferences DataStore.
     */
    suspend fun getContrastLevel() : Userpreferences.ContrastLevel {
        return context.userPreferencesDataStore.data.first().contrastLevel
    }



}

Thank you for reading.

I hope this was useful! And Happy Kooktding !

โš ๏ธ For this recipe, Iโ€™m using Hilt for dependency injection. [Link to article]

You can find the source code here :