How to Implement DataStore in Kotlin Applications

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 :
