Table of contents
- Introduction
- Prerequisites
- What is datastore
- Implementation provided by datastore
- A comparison between shared preference and datastore
- Datastore vs Room
- Let's get coding
- Preference datastore
- Step 1 - Adding datastore dependency
- Step 2 - Naming Preference
- Step 3 - Define your Data Preference class
- Step 4 - ViewModel
- Step 5 - Implementing the Ui
- Proto data store
- How to Implement proto data stores
- Step 1 - Adding dependencies
- Step 2- Define the structure of persisted data in proto file
- Step 3 - Create the Serializer.
- Step 4 - Create Datastore
- Create the ViewModel
- Difference between preference datastore and proto
- Conclusion
- References
Introduction
Information can be retrieved and utilized later by an application. This information can be in various forms, such as text, images, or any other type of data the application requires. There are several data retrieval mechanisms available in Android, such as SharedPreferences, SQLite databases, and external file storage.
Prerequisites
To get along with this article, you should be familiarised with the following:-
What is datastore
The data store is a Jetpack library or API that stores small data sets in a thread-safe and non-blocking manner. It is aimed to replace Shared Preference and is built on Kotlin coroutines and Flows.
Implementation provided by datastore
Datastore provides mainly two implementation
- Preference datastore
Proto datastore
we shall talk more in detail about the two implementations.
A comparison between shared preference and datastore
Shared Pref | Preference Datastore | Proto Datastore |
Async Api but does not update its value cause its UI blocking | Provides safe Async Api using kotlin coroutine and flows | Provides safe Async Api using kotlin coroutine and flows |
Supports Synchronous work | Do operations in the background keeping the UI Thread unblocked | Do operations in the background keeping the UI Thread unblocked |
Throughs Errors | Provides a way of handling the errors using flow signal mechanism | Provides a way of handling the errors using flow signal mechanism |
No type safety | No type safety | Provide type safety |
No data consistency | Provides data consistency and data is updated in read and write operation | Provides data consistency and data is updated in read and write operation |
No Migration support | Provides easy data migration | Provides easy data migration |
Datastore vs Room
Choosing between Room and Datastore can be confusing. Here's a tip to help make the decision: check the size of the data you're working with. If it's complex and requires referential integrity and partial updates, go for Room. Otherwise, choose Datastore.
Let's get coding
"To demonstrate the implementation of the two data stores, we will store the app theme in our local storage such that when a different theme is selected, it changes the application appearance. First, we will begin with the Preference datastore, then proceed to the Proto datastore, and finally, compare the differences between the two."
Preference datastore
This is a data store implementation that stores values in key-value pairs. It provides quick migration from SharedPreferences and does not offer type safety. It also enables asynchronous operations using Kotlin flows and coroutines.
The following steps explain how to implement the Preference datastore.
Step 1 - Adding datastore dependency
In your build.gradle file project level add the following dependency
//DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0"
Step 2 - Naming Preference
Create an object class and give your Preference a name and also store your theme value
object Constants {
const val PREFERENCE_NAME = "YOUR_PREFERENCE_NAME"
val THEME_VALUE = intPreferencesKey(name = "theme_value")
}
Step 3 - Define your Data Preference class
create a class in your /data/local package and create your data preference class that saves the theme and also gets the theme.
class ThemePreference(
private val dataStore: DataStore<Preferences>
) {
val themeFlow: Flow<Int> = dataStore.data
.map { preferences ->
preferences[THEME_VALUE] ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
suspend fun saveTheme(theme: Int) {
dataStore.edit { preferences ->
preferences[THEME_VALUE] = theme
}
}
}
Step 4 - ViewModel
Now in your view model create a function that will save the theme and also get the theme as shown below
class ThemeViewModel(
private val themePreference: ThemePreference
) : ViewModel() {
val themeFlow: Flow<Int> get() = themePreference.themeFlow
fun setTheme(theme: Int) {
viewModelScope.launch {
themePreference.saveTheme(theme)
}
}
}
Step 5 - Implementing the Ui
Since this focuses on changing the app theme all the UI operations will be done on the main activity which has several radio buttons and text views
- Create the instance of theme preference and view model within the
onCreate
method.
val themePreference = ThemePreference(dataStore = dataStore)
val viewModel = ThemeViewModel(themePreference = themePreference)
- Set a variable that collects the theme from the ViewModel and then set it to the Android theme
val theme = viewModel.themeFlow.collectAsState(
initial = Theme.FOLLOW_SYSTEM.themeValue,
context = Dispatchers.Main.immediate
).value
make sure your theme composable takes a theme of type Int.
Thus set your theme like this
DataStoresAndroidTheme(
theme = theme
) {
//codes here.
}
Create a composable with radio buttons and text views and call it in your theme content body.
@Composable fun AppThemeComponent(viewModel: ThemeViewModel) { Column( modifier = Modifier.fillMaxSize().padding(8.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = stringResource(R.string.choose_color), fontSize = 18.sp, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.onBackground ) val radioOptions = listOf<String>("Device settings", "Light Mode", "Dark Mode") val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) } Column(horizontalAlignment = Alignment.CenterHorizontally) { radioOptions.forEach { text -> Row( modifier = Modifier.fillMaxWidth().selectable( selected = (text == selectedOption), onClick = { onOptionSelected(text) when (text) { "Light Mode" -> { viewModel.setTheme(Theme.LIGHT_THEME.themeValue) } "Dark Mode" -> { viewModel.setTheme(Theme.DARK_THEME.themeValue) } else -> { viewModel.setTheme(Theme.FOLLOW_SYSTEM.themeValue) } } } ) .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { val context = LocalContext.current RadioButton( selected = (text == selectedOption), modifier = Modifier.padding(all = Dp(value = 8F)), onClick = { onOptionSelected(text) when (text) { "Light Mode" -> { viewModel.setTheme(Theme.LIGHT_THEME.themeValue) } "Dark Mode" -> { viewModel.setTheme(Theme.DARK_THEME.themeValue) } else -> { viewModel.setTheme(Theme.FOLLOW_SYSTEM.themeValue) } } Toast.makeText(context, text, Toast.LENGTH_SHORT).show() } ) Text( text = text, modifier = Modifier.padding(start = 16.dp) ) } } } Text( text = stringResource(R.string.device_settings), fontSize = 18.sp, fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.onBackground ) } }
Get the preference DataStore implementation from this GitHub repository
Proto data store
Proto data stores use type objects backed by protocal buffers which is type-safe. It relies on a flow error handling mechanism to handle errors. Proto data store provides Async
operations using kotlin coroutines and flows. and also provide a quick data migration.
What is the protocol buffer
A protocol buffer is a language-neutral platform with the mechanism of serializing structured data similar to XML
but it's faster, simpler, and easy to read.
Features of protocol buffer
- language-neutral platform
- serialize structured data
- faster and smaller than xml
- easy to read.
How to Implement proto data stores
Step 1 - Adding dependencies
in your build.gradle
app module and add the protocol buffer id.
id "com.google.protobuf" version "0.8.17"
Still on the same file, add the code below, which should not be in curly braces.
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.19.4"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
Add the protobuf and data store dependencies
//DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0"
// protobuf
implementation "com.google.protobuf:protobuf-javalite:3.19.4"
Sync your build,gradle.
Step 2- Define the structure of persisted data in proto file
Change the view of your project to project view then in app >src >main create a directory proto and in that directory create your .proto
file in this case is theme_prefs.proto.
In proto buff each structure is defined using a message keyword and each member of the structure is defined inside the message based on type and name and it is assigned a one-based order. For this case, my proto buff looks like this.
syntax = "proto3";
option java_package = "com.brandyodhiambo.datastoresandroid";
option java_multiple_files = true;
message ThemePreferences {
int32 theme = 1;
}
The ThemePreference class is generated at a compile time from the message defined in proto thus make sure to rebuild the project.
Step 3 - Create the Serializer.
Move your project to android view and in your data,> local package create Yourserializer. kt
file and extend Serializer<YourProtoBuffMessage>
.The serializer implementation helps in telling the data store how to read and write the data type. It also defines the default value to be returned if there is no data on disk.
object ThemePreferenceSerializer : Serializer<ThemePreferences> {
override val defaultValue: ThemePreferences = ThemePreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): ThemePreferences {
return ThemePreferences.parseFrom(input)
}
override suspend fun writeTo(t: ThemePreferences, output: OutputStream) {
t.writeTo(output)
}
}
Step 4 - Create Datastore
To create datastore use the DataStore<YourPreference>
within the class to create a method to save data and a variable that reads the data from the datastore. for this case, I used a repository to act as my datastore class.
class ThemeRepository(
private val themePreferencesDataStore: DataStore<ThemePreferences>
) {
val themeFlow: Flow<Int> = themePreferencesDataStore.data
.catch { exception ->
if (exception is Exception) {
emit(ThemePreferences.getDefaultInstance())
} else {
throw exception
}
}
.map { themePreferences ->
themePreferences.theme
}
suspend fun saveThemePreference(theme: Int) {
themePreferencesDataStore.updateData { themePreference ->
themePreference.toBuilder().setTheme(theme).build()
}
}
}
Create the ViewModel
The ViewModel implementation is similar to that of the preference Datastore. Here too you will create a method to save data and read data from the DataStore.
class ThemeViewModel(
private val themeRepository: ThemeRepository
) : ViewModel() {
val themeFlow: Flow<Int> get() = themeRepository.themeFlow
fun setTheme(theme: Int) {
viewModelScope.launch {
themeRepository.saveThemePreference(theme)
}
}
}
The UI implementation is just similar to the one implemented above.
Get the proto DataStore implementation from this GitHub repository
Difference between preference datastore and proto
Prefrence Datastore | Proto Datastore |
Data is stored in a key-value pair | uses protocol buffer to store data |
Doesn't provide a type safety | Provides a type save |
Gives a quick migration to shared pref | Allows the use of complex data eg list and enum. |
Conclusion
In conclusion, choosing the right data store is crucial for the success of an application or system. The right choice depends on specific requirements for scalability, performance, data model, data integrity, and security. Regular monitoring and adjustments are needed to ensure the chosen data store meets changing needs. The goal is to find a flexible, efficient, and well-suited data store.