Jetpack Datastore Android: Saving data with preference datastore
Tags: android, kotlin, datastore, mobile
Published: 2020-11-20 23:27:33

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](https://developer.android.com/training/data-storage/shared-preferences) contains a method that runs operation on the ui thread which can possibly block it causing an [ANR](https://developer.android.com/topic/performance/vitals/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](https://developer.android.com/training/dependency-injection/hilt-android) 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](https://github.com/kulloveth/WellnessCity/tree/onboardingSample) to test out the simplified part of that feature while `saveDiet()` 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()` and `fetchDiet()` methods retrieved their respective preference values and acts according to the data present.

* The fetch methods uses [Flow](https://codelabs.developers.google.com/codelabs/advanced-kotlin-coroutines/#0) 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 for `viewModelScope` 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. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/92mkhxr65nqu0gbe89uh.gif)
You can reference the full project [here](https://github.com/kulloveth/WellnessCity)

I hope you find this helpful while implementing preference datastore. Kindly drop your question and suggestion in the comment section