Jetpack Datastore Android: Saving data with preference datastore

kulloveth

Loveth Nwokike

Posted on November 20, 2020

Jetpack Datastore Android: Saving data with preference datastore

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"
Enter fullscreen mode Exit fullscreen mode

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"
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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 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 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()
}
Enter fullscreen mode Exit fullscreen mode
  • 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()
Enter fullscreen mode Exit fullscreen mode

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")
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode
 viewModel.getDiet().observe(requireActivity(), Observer {
                diet = it
            })
Enter fullscreen mode Exit fullscreen mode

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())
            }
        })
    }
Enter fullscreen mode Exit fullscreen mode

OnboardingFragment.kt

  binding?.buttonNext?.setOnClickListener {
                                viewModel.saveOnboarding(true)
                            requireView().findNavController()
                                .navigate(OnboardingFragmentDirections.actionOnboardingFragmentToWelcomeFragment())
                        }
Enter fullscreen mode Exit fullscreen mode

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
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

💖 💪 🙅 🚩
kulloveth
Loveth Nwokike

Posted on November 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related