# How to Implement DataStore in Kotlin Applications

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](https://developer.android.com/topic/performance/vitals/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

```kotlin
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.)](https://protobuf.dev/programming-guides/proto3/)

```kotlin
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.

```kotlin
/**
 * 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 :

```kotlin
/**
 * 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\]](https://code-log.hashnode.dev/use-dependency-injection-with-hilt)

You can find the source code here :

%[https://github.com/patrice-dev/DataStoreRecipe]
