How to integrate Preferences DataStore using Koin for Compose
Hello! 👋 Welcome to this week newsletter! On this post, we'll give a deep dive into the Preferences DataStore
Table of Contents
What is DataStore?
Why should you use DataStore instead of SharedPreferences?
When should you use DataStore?
Types of DataStore
Use Case: Preferences DataStore to save User Settings
Setup
Creating the model
Setting up the Repository
The ViewModel
Injecting Preferences DataStore using Koin
Creating the UI
The app in action
References.
What is DataStore?
DataStore is a Jetpack component that provides a solution to store key-value pairs or typed objects using protocol buffers. DataStore is meant to replace SharedPreferences to store data.
Why should you use DataStore instead of SharedPreferences?
Uses Kotlin Coroutines and Flows to support asynchronous programming
Provides a Type-Safe option through Proto DataStore
Supports Observable data
Supports complex data structures through Proto DataStore
Provides test implementations
Is Thread-safe
When should you use DataStore?
DataStore is the ideal solution to store small and simple datasets. It won't be useful to save large or complex datasets. If you require supporting larger amounts of data, partial updates, or referential integrity, consider using a database solution like Room.
Types of DataStore
There are two implementations of DataStore:
Preferences DataStore: Is perfect to store and access data using keys. It doesn't require a predefined schema like a database, but it doesn't provide type safety.
Proto DataStore: You can use this implementation to stores data as instances of a custom data type. It's more complex, since it needs a defined schema using protocol buffers, but it provides type safety.
In this post, we'll walk through a use case for the Preferences DataStore.
Use Case: Preferences DataStore to save User Settings
For this use case, we are integrating Preferences DataStore to store the following User settings:
Username
Age
First time user
We are using Koin as the Dependency Injection framework
Setup
The first step is adding the dependencies to your app build.gradle:
build.gradle//navigation
implementation "androidx.navigation:navigation-compose:2.7.7"
//Koin
def koin_version = "3.1.6"
implementation "io.insert-koin:koin-androidx-compose:$koin_version"
implementation "io.insert-koin:koin-test:$koin_version"
implementation "io.insert-koin:koin-core:$koin_version"
implementation "io.insert-koin:koin-androidx-navigation:$koin_version"
implementation "io.insert-koin:koin-android:$koin_version"
//DataStore
implementation "androidx.datastore:datastore-preferences:1.1.1"
Creating the model
To start adding the Preferences DataStore, you first need to decide which is the data that you want to store. For that, the best solution is to create a data class
that represents your model.
Let's create a data class
with three properties:
data class UserSettings(
val username: String,
val age: Int,
val firstTimeUser: Boolean
)
This class allows you to handle the information that you're storing in the DataStore.
Setting up the Repository
The next step is to create a repository. With the repository, you can expose methods to manage your DataStore. For example, storing and getting information from your DataStore.
The best practice is to create an interface
to expose these methods.
interface PreferenceStore{
fun getSettings() : Flow<UserSettings>
suspend fun saveSettings(settings : UserSettings)
companion object{
//createing the Preferences.Key
val USERNAME = stringPreferenceKey("username")
val AGE = intPreferenceKey("age")
val FIRST_TIME_USER = booleanPreferenceKey("fisrtTimeUser")
}
}
Preferences DataStore stores with key-value pairs. The key must be of type Preferences.Key<T>
, where T
is the type of the value associated to that key. Your value must be one of the following data types, with its respective constructor:
Boolean →
booleanPreferenceKey()
Int →
intPreferenceKey()
Long →
longPreferenceKey()
Float →
floatPreferenceKey()
String →
stringPreferenceKey()
Set →
stringSetPreferenceKey()
This would be the implementation of the repository:
class PreferenceStoreImpl(
private val datastore : DataStore<Preferences>
) : PreferenceStore{
override fun getSettings() : Flow<UserSettings> =
datastore.data.map{preference ->
val username = preference[PreferenceStore.USERNAME] ?: ""
val age = preference[PreferenceStore.AGE] ?: 0
val firstTime = preference[PreferenceStore.FIRST_TIME_USER] ?: true
UserSettings(username,age,firstTime)
}
override suspend fun saveSettings(settings : UserSettings){
datastore.edit{preference ->
preference[PreferenceStore.USERNAME] = settings.username
preference[PreferenceStore.AGE] = settings.age
preference[PreferenceStore.FIRST_TIME_USER] = settings.firstTimeUser
}
}
}
The ViewModel
Before starting with the ViewModel, let's create a generic UiState class to handle the different states of the download.
sealed class UiState<out T>{
object LOADING : UiState<Nothing>
data class SUCCESS<T>(val information : T) : UiState<T>
data class ERROR(val error : Exception) : UiState<Nothing>
}
The ViewModel provides the interface between the data layer (DataStore) and the UI.
class DataStoreViewModel(
private val preferencesStore: PreferencesStore,
private val dispatcher : CoroutineDispatcher
) : ViewModel() {
private val _userState : MutableStateFlow<UiState<UserSettings>> = MutableStateFlow(UiState.LOADING)
val userSettings = _userState.asStateFlow()
fun getUserSettings() {
viewModelScope.launch(dispatcher) {
_userState.value = UiState.LOADING
try {
preferencesStore.getSettings().collect{
_userState.value = UiState.SUCCESS(it)
}
} catch (e: Exception){
_userState.value = UiState.ERROR(e)
}
}
}
fun updateUserSettings(username : String, age: Int){
viewModelScope.launch(dispatcher) {
val settings = UserSettings(username,age,false)
preferencesStore.saveSettings(settings)
_userState.value = UiState.SUCCESS(settings)
}
}
}
Injecting Preferences DataStore using Koin
To be able to inject your DataStore module into the rest of your application with Koin, you need to follow 3 simple steps:
Create your modules
private const val USER_SETTINGS = "user_settings"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = USER_SETTINGS
)
val dataModule = module {
//data store
single<PreferencesStore> {
PreferencesStoreImpl(androidContext().dataStore)
}
}
val viewModelModule = module {
viewModel {
DataStoreViewModel(
preferencesStore = get(),
dispatcher = Dispatchers.IO
)
}
}
Create your application class
class PreferencesApp : Application() {
override fun onCreate() {
super.onCreate()
//starting Koin
startKoin {
androidContext(this@PreferencesApp)
//add all the modules needed
modules(
dataModule,
viewModelModule
)
}
}
}
Add your application class to the
AndroidManifest.xml
using the line:android:name=".PreferencesApp"
Creating the UI
Now, it's time to create the UI for your app. To showcase the functionality of the DataStore, we are going to create 2 composable functions, each one will represent a different screen:
A function to register the user.
A function to show the user's information.
The user should enter their information in the register screen. Once this process is completed, this screen shouldn't appear again.
@Composable
fun RegisterScreen(
viewModel : DataStoreViewModel,
onClicked : () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
var username by remember {
mutableStateOf("")
}
var age by remember {
mutableStateOf("")
}
Text(text = "Username")
OutlinedTextField(
value = username,
onValueChange = {username = it},
label = { Text(text = "Username")}
)
Text(text = "Age")
OutlinedTextField(
value = age,
onValueChange = {age = it},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
)
Button(
onClick = {
viewModel.updateUserSettings(username,age.toInt())
onClicked()
}
) {
Text(text = "Save")
}
}}
To show the user's information, we'll create a screen with two text views, one for the username and one for the age:
@Composable
fun HomeScreen(
viewModel: DataStoreViewModel
){
when(val settingsState = viewModel.userSettings.collectAsState().value){
is UiState.ERROR -> {}
UiState.LOADING -> {}
is UiState.SUCCESS -> {
Column(modifier = Modifier.fillMaxWidth()) {
val settings = settingsState.information
Text(text = "Welcome, ${settings.username}")
Text(text = "Your age: ${settings.age}")
}
}
}
}
Finally, we need to add our composable functions to our Main Activity. Since our composable functions require the ViewModel as a parameter, we need to inject it using the Koin by viewModel()
delegate.
To handle the behavior of the screens we are creating a Navigation Graph. Let me know in the comments if you want me to talk more about Navigation Graphs in Compose.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DataStoreExampleTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navController = rememberNavController()
//koin injection of viewmodel
val dataViewModel : DataStoreViewModel by viewModel()
dataViewModel.getUserSettings()
DataStoreGraph(
viewModel = dataViewModel,
navController = navController,
modifier = Modifier
)
}
}
}
}
}
@Composable
fun DataStoreGraph(
viewModel: DataStoreViewModel,
navController: NavHostController,
modifier: Modifier
){
NavHost(
navController = navController,
modifier = modifier,
startDestination = "register"
){
composable("register"){
when(val settings = viewModel.userSettings.collectAsState().value){
is UiState.ERROR -> {}
UiState.LOADING -> {}
is UiState.SUCCESS -> {
if(settings.information.firstTimeUser){
RegisterScreen(viewModel = viewModel) {
navController.navigate("homepage")
}
} else {
navController.navigate("homepage")
}
}
}
}
composable("homepage"){
HomeScreen(viewModel = viewModel)
}
}
}
The app in action
Finally, here is the end result:
As you can see, the second time that the app is open, the user's information is already saved in the device.
You can find the app source code on GitHub.
I hope you’ve enjoyed this week post. Please subscribe of you want to see similar posts.