DEV Community

loading...

Jetpack DataStore and How to implement it.

Azat Sayan
Android dev & enthusiast. Love Kotlin and Android related stuff.
・3 min read

Jetpack DataStore is a new improved library and built on Coroutines and Flow which aims to replace the SharedPreferences. So What are the advantages over SharedPreferences; Data is stored asynchronously, consistently, and transactionally.

If we look a little deeper into our topic;

  • DataStore provides asynchronous API(via Flow) for storing and reading data but SharedPreferences provides async API only for reading data.

  • DataStore is safe to call on the UI thread because it uses Dispatchers.IO, but SharedPreferences blocks the UI thread. apply() and commit() have no mechanism of signaling errors and apply() will block the UI thread on fsync() often becoming a source of ANRs.

  • DataStore is safe from runtime exceptions, but SharedPreferences throws parsing errors in runtime exceptions.

And also DataStore has two different implementations:
Preferences DataStore and Proto DataStore

Preferences DataStore – stores and accesses data via key-value pairs like SharedPreferenecs, but this implementation does not provide type safety.

Proto DataStore – stores data as custom objects and requires to define a schema using protocol buffers. Also, it provides type safety.

Note: Today we're just looking at the implementation of the Preferences DataStore.

Screenshot_2020-09-12 Prefer Storing Data with Jetpack DataStore

First of all, we need to add some dependencies;

    // Preferences DataStore
    implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01" 

    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
    implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" 

Ok, Let's suppose we store a user's informations which are name and surname. Her is our UI;

nameandsurname

Next, we're gonna create a named UserInfo class which takes a context in the constructor.

class UserInfo(context: Context) { 

below, we create a dataStore and we need to give it a name.

class UserInfo(context: Context) {

    val dataStore = context.createDataStore("user_info")

}

Then we should create some keys, so to do this we're gonna create a companion object.

we create two keys which are named USER_NAME_KEY && USER_SURNAME_KEY, they're gonna be equal to preferencesKey of type String and then we need to give them the actual name USER_NAME && USER_SURNAME.

 class UserInfo(context: Context) {

    val dataStore = context.createDataStore("user_info")

    companion object {
        val USER_NAME_KEY = preferencesKey<String>("USER_NAME")
        val USER_SURNAME_KEY = preferencesKey<String>("USER_SURNAME")
    }
} 

Next, we create a suspend function to save our data and it takes two arguments. We call dataStore.edit inside here we store values.

  suspend fun saveUserInfo(name: String, surname: String ) {
        dataStore.edit {
            it[USER_NAME_KEY] = name
            it[USER_SURNAME_KEY] = surname
        }
    }

So, that will take care of storing data to the key we have assigned it.

Now we create two flow which will help us to retrieve data. And we map them to our keys also we check if they're null we return an empty String ("")

val userNameFlow: Flow<String> = dataStore.data.map {
        it[USER_NAME_KEY] ?: ""
    }

val userSurnameFlow: Flow<String> = dataStore.data.map {
        it[USER_SURNAME_KEY] ?: ""
    } 

Okay until now we're done with UserInfo. Now we're going to MainActivity.

First, we create a lateinit var for UserInfo and 2 variables for name and surname.

 lateinit var userInfo: UserInfo

 var name = ""
 var surname = ""

And we're going to onCreate method to instantiate userInfo and write two methods that retrieve and save data.

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        userInfo = UserInfo(this)

        saveData()

        observeData()
    }

After this one, we're going to saveData()
and inside here, as you know we created a suspend function in UserInfo class. So, this has to be done in CoroutineScope. We're just gonna call GlobalScope.launch and inside this block code we call saveUserInfo() method.

 private fun saveData() {
        buttonSave.setOnClickListener {
            name = etName.text.toString()
            surname = etSurname.text.toString()

            GlobalScope.launch {
                userInfo.saveUserInfo(name, surname)
            }
        }
    }

After saving data we're going to observe our LiveDatas and retrieve them to write to our TextViews.

private fun observeData() {
        userInfo.userNameFlow.asLiveData().observe(this, {
            name = it
            tvName.text = "Saved name: $it"
        })

        userInfo.userSurnameFlow.asLiveData().observe(this, {
            surname = it
            tvSurname.text = "Saved surname: $it"
        })
    }

Alt Text

Alt Text

  • In additionally, as I mentioned before, DataStore is safe from runtime exceptions it reads data from a file, IOExceptions are thrown when an error occurs while reading data so, we can handle these exceptions before map()
val userNameFlow: Flow<String> = dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                Log.d("DataStore", exception.message.toString())
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map {
            it[USER_NAME_KEY] ?: ""
        }

Yeah, that's all.

Discussion (0)

Forem Open with the Forem app