Jetpack datastore is a new data storage solution added to the jetpack library and is currently in alpha. It provides two types of implementation:
Preference Datastore
- Store and access data using keys
- Stores data asynchronously
- Uses coroutine and flow
- Safe to call on ui thread
- Safe from runtime exception
- Does not provide type-safety
Proto DataStore
- Allows storing data as custom data type
- Provides type-safety
- Stores data asynchronously
- Uses coroutine and flow
- Safe to call on ui thread
- Safe from runtime exception
- Defines schema using protocol buffers
- They are faster, smaller, simpler, and less ambiguous than XML and other similar data formats.
Jetpack datastore is aimed at replacing SharedPreferences. SharedPreference contains a method that runs operation on the ui thread which can possibly block it causing an ANR error but with datastore operations are run asynchronously off the main thread using coroutine and flow. This is the first part of the post and we will be looking at preference datastore.
We will be using a sample app that includes a page displaying food types depending on selected diet
To use preference datastore we need to add the dependency
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha03"
Hilt is used as a dependency injection library in the project so lets create the datastore module
DatastoreModule.kt
@Module
@InstallIn(ApplicationComponent::class)
class DataStoreModule {
@Provides
fun provideOnBoardPreference(@ApplicationContext context: Context): DataStore<Preferences>{
return context.createDataStore(
name = "com.wellnesscity.health"
)
}
}
We have created a datastore module file that creates Preference Datastore dependency with the name com.wellnesscity.health
. Next we create a preference manager class
DataStorePreferences.kt
class DataStorePreference @Inject constructor(private val preferences:DataStore<Preferences>) {
suspend fun saveOnboarding(save:Boolean) {
preferences.edit { onboardPreference->
onboardPreference[preferencesKey<Boolean>(ONBOARD_KEY)] = save
}
}
fun fetchOnboarding() = preferences.data.map {onboardPreference->
onboardPreference[preferencesKey<Boolean>(ONBOARD_KEY)] ?: false }
suspend fun saveDiet(diet:String){
preferences.edit {dietPreference->
dietPreference[preferencesKey<String>(DIET_KEY)] = diet
}
}
fun fetchDiet() = preferences.data.map {dietPreference->
dietPreference[preferencesKey<String>(DIET_KEY)]?:"vegetarian"
}
companion object{
const val ONBOARD_KEY = "onBoard"
const val DIET_KEY = "diet"
}
}
In the DatastorePreference;
The methods
saveOnboarding()
is used to ensure the onboarding page only displays once on the apps first install, you can check onboarding branch to test out the simplified part of that feature whilesaveDiet()
stores selected diet to the preference file so that when the user exits the page and come back they will still be seeing the recipe for the last diet they selected.The save methods includes the suspend qualifier which is a coroutine key word that moves operation out of the main thread except specified otherwise.
The
edit()
function is used to write value into the preference file providing the type of value and a key to reference it when necessary.The
fetchOnboarding()
andfetchDiet()
methods retrieved their respective preference values and acts according to the data present.The fetch methods uses Flow to retrieve the value ensuring that operations are run off the main thread.
Next we are going to provide this class to the ViewModel classes that need them.
OnboardingViewModel.kt
class OnboardingViewModel @ViewModelInject constructor(private val preference: DataStorePreference) :
ViewModel() {
fun saveOnboarding(save: Boolean) {
viewModelScope.launch {
preference.saveOnboarding(save)
}
}
fun fetchOnboarding() = preference.fetchOnboarding().asLiveData()
}
- Since the
saveOnboarding()
from the DatastorePreference class is a suspend function it can only be called from a coroutine scope hence the need forviewModelScope
since we are in a viewModel class. - With the help of the lifecycle extension we are able to convert the
fetchOnboarding()
from flow to Livedata so we can observe from our Fragment class.
same applies to the DietViewModel class
fun saveDiet(diet:String){
viewModelScope.launch {
preference.saveDiet(diet)
}
}
fun getDiet() = preference.fetchDiet().asLiveData()
Now lets provide these ViewModel classes to Fragments that need them
DieFragment.kt
fun setupDiet(id: Int) {
when (id) {
R.id.vegan -> {
viewModel.saveDiet("vegan")
}
R.id.whole -> {
viewModel.saveDiet("Whole30")
}
R.id.gf -> {
viewModel.saveDiet("Gluton Free")
}
R.id.vegy -> {
viewModel.saveDiet("Vegetarian")
}
R.id.ketogen -> {
viewModel.saveDiet("Ketogenic")
}
R.id.lactovegy -> {
viewModel.saveDiet("Lacto-vegetarian")
}
R.id.ovovegy -> {
viewModel.saveDiet("Ovo-vegetarian")
}
R.id.pesce -> {
viewModel.saveDiet("Pescetarian")
}
R.id.palio -> {
viewModel.saveDiet("Paleo")
}
R.id.primal -> {
viewModel.saveDiet("Primal")
}
}
}
viewModel.getDiet().observe(requireActivity(), Observer {
diet = it
})
We are calling the saveDiet()
from DietVieModel class to save each selected diet and then we retrieve the diet by observing the getDiet()
method.
Same applies to the Onboarding,
SplashFragment.kt
viewModel.fetchOnboarding().observe(requireActivity(), Observer {
if (it == true) {
view?.findNavController()
?.navigate(SplashFragmentDirections.actionSplashFragmentToWelcomeFragment())
} else {
requireView().findNavController()
.navigate(SplashFragmentDirections.actionSplashFragmentToOnboardingFragment())
}
})
}
OnboardingFragment.kt
binding?.buttonNext?.setOnClickListener {
viewModel.saveOnboarding(true)
requireView().findNavController()
.navigate(OnboardingFragmentDirections.actionOnboardingFragmentToWelcomeFragment())
}
We Observe the fetchOnboarding()
from the OnboardingViewModel class, depending on the value returned we either navigate to the onboarding page or navigate to the Welcome page while in the OnboardingFragment the value is saved to true once the finish button present on the final page for onboarding is clicked.
You can reference the full project here
I hope you find this helpful while implementing preference datastore. Kindly drop your question and suggestion in the comment section
Top comments (4)
Great!
Question: did you create
unit tests
for the DataStorePreference class?how to store and retrieve DATA CLASS
Is there any helper methods for testing? If not what is your suggestions for writing UI test that uses DataStore?
I haven't really thought about the testing part of it but using a thread or Idling resource should do the trick.