BEST way to implement Preference DataStore
In this blog post, we will demonstrate how to implement Preference DataStore
in Android by utilizing a simple counter functionality. The application will use Jetpack Compose for its user interface, while DataStore
will be employed to persist the counter value across sessions. We will examine the implementation step by step, providing detailed explanations of each component to ensure a comprehensive understanding of how DataStore
can be applied to manage persistent key-value data in an Android project.
What is DataStore?
DataStore
is a modern data storage solution introduced by Android as an alternative to the traditional SharedPreferences
API. It offers a more efficient and robust way to store key-value pairs or typed objects. DataStore
is built to handle asynchronous data processing using Kotlin Coroutines and Flow, making it especially useful for modern Android development.
There are two types of DataStore
:
- Preference DataStore: Stores simple key-value pairs, similar to SharedPreferences.
- Proto DataStore: Stores structured data using Protocol Buffers, ideal for more complex scenarios.
Benefits of DataStore
- Asynchronous API: DataStore operates with Kotlin Coroutines and Flow, ensuring smooth, non-blocking I/O operations.
- Thread-Safe: DataStore is designed to handle data access across multiple threads, avoiding potential race conditions.
- Error Handling: Built-in mechanisms to handle errors (such as
catch
) provide a more reliable solution for data persistence. - Migration from SharedPreferences: DataStore provides an easy migration path from SharedPreferences, making it simpler to upgrade legacy systems.
- Type Safety with Proto DataStore: Proto DataStore enables storing strongly-typed data, offering more control and flexibility compared to key-value pairs.
Cons of DataStore
- Learning Curve: For developers unfamiliar with Kotlin Coroutines and Flow, there might be a steeper learning curve compared to SharedPreferences.
- Overhead for Simple Use Cases: For very simple key-value storage, the additional boilerplate code required for DataStore might seem excessive.
- No Direct UI Observability: Unlike
LiveData
withSharedPreferences
,DataStore
relies onFlow
, which is more complex to observe in traditional Android applications without Compose.
Why use DataStore?
Given its flexibility, safety, and modern design, DataStore is an excellent choice for developers looking to build robust Android applications.
In the next section, we’ll demonstrate its use by implementing a simple counter functionality.
First, let’s set up the necessary dependencies in your build.gradle.kts
file.
implementation("androidx.datastore:datastore:1.0.0")
implementation("androidx.datastore:datastore-rxjava2:1.0.0") // optional - RxJava2 support
implementation("androidx.datastore:datastore-rxjava3:1.0.0") // optional - RxJava3 support
implementation("androidx.datastore:datastore-preferences:1.0.0")
The MainScreen
composable provides the user interface for the counter. It consists of a text to display the counter value, a button to increment the value, and a floating action button to reset it.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.codebyzebru.datastoredemo.factory.MainViewModelFactory
import com.codebyzebru.datastoredemo.helper.DataStoreHelper
import com.codebyzebru.datastoredemo.viewmodel.MainViewModel
@Composable
fun MainScreen(
dataStoreHelper: DataStoreHelper
) {
val mainViewModel: MainViewModel = viewModel(factory = MainViewModelFactory(dataStoreHelper))
val counter by mainViewModel.counter.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "$counter",
modifier = Modifier,
fontSize = 36.sp
)
Button(
onClick = { mainViewModel.incrementCounter() },
modifier = Modifier.padding(top = 10.dp)
) {
Text(
text = "Increment\ncounter",
textAlign = TextAlign.Center,
)
} // Button
FloatingActionButton(
modifier = Modifier.padding(top = 10.dp),
onClick = { mainViewModel.resetCounter() }
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = "Resent button",
modifier = Modifier.size(35.dp)
)
}
} // Column
}
This UI presents the current counter value and offers buttons to modify it. We are using a ViewModel to manage the state, which we will discuss shortly.
To manage interactions with the DataStore
, we'll create a DataStoreHelper
class that encapsulates all the logic for reading, updating, and resetting the counter value.
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
class DataStoreHelper(private val context: Context) {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "counter_preferences")
companion object {
var counterValue = intPreferencesKey("counterValue")
}
fun getOnCounterValue(): Flow<Int> {
return context.dataStore.data.catch {
emit(emptyPreferences())
}.map { preference ->
preference[counterValue] ?: 0
}
}
suspend fun incrementCounterValue(count: Int) {
context.dataStore.edit { mutablePreferences ->
mutablePreferences[counterValue] = count
}
}
suspend fun resetCounterValue() {
context.dataStore.edit { mutablePreferences ->
mutablePreferences[counterValue] = 0
}
}
}
Here, we:
- Define a
DataStore
to hold the preferences in the app. - Use the
intPreferencesKey
to store and retrieve the counter value. - Provide functions to get, increment, and reset the counter value using
Flow
andCoroutines
.
Now time for ViewModel, The MainViewModel
class will act as a bridge between the UI and the DataStore. It will observe the current counter value and will provide methods to update or reset it.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.codebyzebru.datastoredemo.helper.DataStoreHelper
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class MainViewModel(
private val dataStoreHelper: DataStoreHelper
): ViewModel() {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter.asStateFlow()
init {
viewModelScope.launch {
dataStoreHelper.getOnCounterValue().collect {
_counter.value = it
}
}
}
fun incrementCounter() {
viewModelScope.launch {
val newCounter = _counter.value + 1
dataStoreHelper.incrementCounterValue(newCounter)
}
}
fun resetCounter() {
viewModelScope.launch {
dataStoreHelper.resetCounterValue()
}
}
}
In the init{}
block, we launch a coroutine to observe changes to the counter in real-time. The incrementCounter()
and resetCounter()
methods are responsible for updating the DataStore
with new values.
Since we need to pass DataStoreHelper
to MainViewModel
, we use a ViewModelFactory
to instantiate it.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.codebyzebru.datastoredemo.helper.DataStoreHelper
import com.codebyzebru.datastoredemo.viewmodel.MainViewModel
class MainViewModelFactory(
private val dataStoreHelper: DataStoreHelper
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MainViewModel(dataStoreHelper) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
In this example, we use a
ViewModelFactory
to instantiateMainViewModel
becauseMainViewModel
requires a parameter (DataStoreHelper
). By default, Android'sViewModelProvider
doesn't support passing parameters to ViewModel constructors.A
ViewModelFactory
solves this problem by allowing us to create custom instances of ViewModel. It defines how to construct the ViewModel when the system needs one, ensuring that ourMainViewModel
is initialized with the correct dependencies (in this case,DataStoreHelper
). Without this factory, we wouldn’t be able to passDataStoreHelper
into theMainViewModel
, which would break the flow of our app.For beginners, think of the
ViewModelFactory
as a special helper that tells Android how to create your ViewModel when it has custom dependencies.
Finally, we tie everything together in MainActivity
, where we initialize the UI and pass the DataStoreHelper
to the MainScreen
:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.codebyzebru.datastoredemo.helper.DataStoreHelper
import com.codebyzebru.datastoredemo.ui.screen.MainScreen
import com.codebyzebru.datastoredemo.ui.theme.DataStoreDemoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
DataStoreDemoTheme {
MainScreen(DataStoreHelper(this))
}
}
}
}
Conclusion
In this blog, we demonstrated how to implement Android’s Preference DataStore
by building a simple counter functionality. We explored the key components such as DataStoreHelper
, MainViewModel
, and how these integrate with Jetpack Compose to create a seamless, persistent user experience.
This setup highlights the benefits of using DataStore
for managing small, key-value pairs in a modern Android application, providing asynchronous operations, better error handling, and improved thread safety.
I hope this article has provided valuable insights and assistance for your Android journey. Your support means a lot to me, so if you found this content helpful, please start following me and don’t hesitate to show some love with a hearty round of applause!👏
Your feedback fuels my passion for creating quality content. For any Android queries or just to connect, reach out on LinkedIn and Twitter.
Thanks for reading — looking forward to staying in touch!
Happy Coding!!