Data persistence is a very important aspect of app development. It could make or break the user experience. Storing and managing app settings/data is essential for creating a responsive, reliable and effective user experience. DataStore is a toolkit that helps address this issue. In this guide, I will try to answer What? Why? and how? of DataStore.
What is DataStore?
DataStore is a part of the Jetpack Libraries provided by Google. It’s a simple framework that provides an asynchronous way to store and retrieve both simple and complex data types. It’s the perfect candidate for dealing with app settings, user preferences or any other kind of persistent information.
Why DataStore?
DataStore was introduced as a solution to some of the problems or limitations faced by SharePreferences and SQLite.
- Asynchronous Operations: All operations are done in an async fashion when it comes to datastore ensuring that the app never goes into ANR state.
- Strongly Typed Data: SharedPreferences was great but only supported primitive data types, which meant you had to use Gson or Moshi to convert complex data into a JSON and store it as a string and back to an object later on. DataStore solves this by allowing the storing and retrieving of structured data using the Kotlin data class.
- Type Safety: DataStore provides type safety, reducing the risk of runtime crashes and errors which might occur due to type casting.
- Modern API: With DataStore you have the option to leverage Kotlin’s modern features like coroutines and flows.
- Data Consistency: DataStore makes sure that data is read and written atomically. Meaning it does not have common issues like partial writes.
- Observability: Since DataStore enables modern Kotlin features you can easily observe the changes in the values using flow.
DataStore Types
There are two options when it comes to DataStores, i.e., Preferences DataStore and Proto DataStore.
Preferences DataStore
Preferences DataStore is very similar to SharedPreferences. It’s meant to store simple key-value pairs. However, with the benefit of being built on modern principles and providing all the above-mentioned advantages. Use Preferences DataStore when you need to store simple key-value pairs like user preferences, app settings or some developer flags.
Proto DataStore
Proto DataStore, is more suitable for storing complex data types. It uses protobuf to serialize and deserialize data. It’s the perfect candidate if you want to store more complex data types. Use Proto DataStore when you have to store complex data types and require strong type safety for your data.
Setting Up
Adding DataStore to your android project is fairly straightforward. Head over to the app’s build.gradle
file and add the dependency like you would with any other library.
// Add these lines to your app module's build.gradle file | |
dependencies { | |
// For Preferences DataStore | |
implementation "androidx.datastore:datastore-preferences:1.0.0" | |
// For Proto DataStore | |
implementation "androidx.datastore:datastore-core:1.0.0" | |
} |
Using DataStore
We will be using Jetpack compose for these examples but should be very similar for traditional view systems as well.
Storing simple data
Let’s start by storing simple data using Preference DataStore. Take into consideration that you need to store something simple like the app theme the user has selected via a toggle button in the app settings. Toggle on means the app goes into a dark theme and toggle off means the app goes into a light theme.
// Define a DataStore for user preferences | |
val dataStore: DataStore<Preferences> = context.createDataStore(name = "user_preferences") | |
// Define a key for the dark mode | |
val DARK_MODE_KEY = booleanPreferencesKey("dark_mode") | |
// Function to store the toggle value | |
suspend fun storeDarkMode(isOn: Boolean) { | |
dataStore.edit { preferences -> | |
preferences[DARK_MODE_KEY] = isOn | |
} | |
} | |
// Function to retrieve the dark mode value | |
val darkModeFlow: Flow<Boolean?> = dataStore.data.map { preferences -> | |
preferences[DARK_MODE_KEY] | |
} |
Job Offers
In the above code, we first declared a DataStore instance(dataStore) for storing user preferences. We then declared a key (DARK_MODE_KEY) for the value we wanted to store. Then we have a method that would store the value into the DataStore (storeDarkMode) and lastly declare a flow (darkModeFlow) which would retrieve the currently stored value.
Here is an example of how you would retrieve the value and update the value.
// Collect the darkModeFlow and convert it to a State | |
val darkModeState by darkModeFlow.collectAsState(initial = false) | |
// Display the currently selected theme in your Composable | |
Text(text = "is the app in dark mode: ${darkModeState.value}") | |
// Update teh darkMode value | |
storeDarkMode(true) |
Preference DataStore is great when it comes to simple key-value pairs but when it comes to complex data structures Proto Datastore really thrives. To start using Proto DataStore you must follow these steps.
Define a protobuf schema for your data.
syntax = "proto3"; | |
message UserProfile { | |
string username = 1; | |
int32 age = 2; | |
// Add more fields as needed | |
} |
Generate Kotlin code from the schema using protobuf-gradle-plugin
Add the following to your project.
plugins { | |
id "com.google.protobuf" version "0.9.4" | |
} | |
dependencies { | |
implementation "com.google.protobuf:protobuf-javalite:3.24.3" | |
} | |
protobuf { | |
protoc { | |
artifact = "com.google.protobuf:protoc:3.24.3" | |
} | |
// Generates the java Protobuf-lite code for the Protobufs in this project. See | |
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation | |
// for more information. | |
generateProtoTasks { | |
all().each { task -> | |
task.builtins { | |
java { | |
option 'lite' | |
} | |
} | |
} | |
} | |
} |
Rebuild the project. This should generate a file under app/build/generated/source/proto
called UserProfile
which should have all the methods needed for storing and retrieving values.
Use the generated Kotlin class with Proto DataStore.
// Define a DataStore | |
val dataStore: DataStore<UserProfile> = context.createDataStore( | |
fileName = "user_profile.pb", | |
serializer = UserProfileSerializer // Generated serializer class | |
) | |
// Function to retrive the user profile | |
override suspend fun getUserProfile(): Flow<UserProfle> { | |
return dataStore.data.map { protoBuilder -> | |
protoBuilder | |
} | |
} | |
// Function to store the user profile | |
override suspend fun saveUserProfile(userProfile: UserProfile) { | |
dataStore.updateData { store -> | |
store.toBuilder() | |
.setUserProfile(userProfile) | |
.build() | |
} | |
} |
Testing DataStore
Testing is a crucial part of development and DataStore is no exception.
Unit Testing
You can easily write unit tests for DataStore operations by creating a test instance of DataStore. With the test DataStore instance, you can write tests for functions that interact with DataStore. Here is an example:
// Create a test DataStore for Preferences DataStore | |
val testContext = ApplicationProvider.getApplicationContext<Context>() | |
val testPreferencesDataStore = testContext.createDataStore( | |
name = "test_preferences", | |
serializer = PreferencesSerializer | |
) |
Mocking
You can also mock DataStore using frameworks like Mockito or MockK. This allows you to control the data returned by DataStore and simulate different scenarios of testing.
// Mock the DataStore | |
val mockDataStore = mockk<DataStore<Preferences>>() | |
// Define behavior for DataStore functions | |
coEvery { mockDataStore.data } returns flowOf(mockedPreferences) |
Conclusion
In this guide, we explored What is DataStore? Why should one pick DataStore? and how to use DataStore in Jetpack compose? DataStore, being a part of the Jetpack libraries, empowers developers with the tools to manage and store data efficiently and reliably. Happy Coding!!
This article was previously published on proandroiddev.com