Preferences DataStore In Android

Introduction

Hi friends, In this article you will learn about how to use Preferences DataStore in Android. DataStore is a new and improved data storage solution aimed at replacing SharedPreferences. DataStore has two types-

  • Proto DataStore
  • Preferences DataStore

Proto DataStore 

This type of DataStore is designed for storing strongly typed data using Protocol Buffers (protobuf). It allows you to define your data structure using protobuf and then serialize and deserialize that data to and from the data store. This is useful for more complex data structures.

Preferences DataStore

Preferences DataStore stores data into key-value pairs. Data is stored asynchronously, consistently, and transactionally, overcoming some of the drawbacks of SharedPreferences. Now Google recommends using DataStore for persistent storage instead of SharedPreferences. 

SharedPreferences VS Preferences DataStore VS Proto DataStore

SharedPreferences VS Preferences DataStore VS Proto DataStore 

Let's make an alphabet App which having two layout designs. We are going to use Preferences DataStore to store user layout preferences.

Let's start with Android Studio

Step 1. Create a new project and select Empty Views Activity.

Create app 

Step 2. Please add the required dependency in build.gradle.

// navigation dependency 
implementation ("androidx.navigation:navigation-fragment-ktx:2.5.2")
implementation ("androidx.navigation:navigation-ui-ktx:2.5.2")

// data store dependency
implementation ("androidx.datastore:datastore-preferences:1.0.0")
    
// live data dependency
implementation ("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1")

Step 3. Add the below plugin in the plugins section in build.gradle(app level).

id ("kotlin-android")
id ("androidx.navigation.safeargs.kotlin")

Step 4. Go to build.gradle(project level) and add the below classpath.

buildscript{
    dependencies{
        classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.2")
    }
}

After this, Please press the Sync Now button.

Step 5. Create an empty fragment named LetterListFragment.kt and edit the fragment_letter_list.xml.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LetterListFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/alphabet_recylerview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>

Step 6. Go to the res folder create a new directory named navigation and create a new Navigation Resource file named nav_graph.xml.

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/letterListFragment">

    <fragment
        android:id="@+id/letterListFragment"
        android:name="com.example.preferencedatastoreexample.LetterListFragment"
        android:label="fragment_letter_list"
        tools:layout="@layout/fragment_letter_list" />
    
</navigation>

Step 7. Go to activity_main.xml and add the below code.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"
        />

</RelativeLayout>

Step 8. Go to MainActivity.kt and add the below code

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController

class MainActivity : AppCompatActivity() {

    private lateinit var navController: NavController


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

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }

    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp() || super.onSupportNavigateUp()
    }

}

Here we set up a navigation controller, associate it with a navigation host fragment, and configure the action bar to reflect the app's navigation state. The onSupportNavigateUp function handles the "up" button press, facilitating navigation within the app.

Step 9. Create a new Layout Resource File named alphabet_item_layout.xml and add the below code.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:id="@+id/alphabet_cardview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardBackgroundColor="#0097A7"
        app:cardCornerRadius="5dp"
        android:layout_margin="10dp">

        <TextView
            android:id="@+id/alphabet_textview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="A"
            android:textColor="@color/white"
            android:gravity="center"
            android:textSize="20sp"/>
        
    </androidx.cardview.widget.CardView>
    
</RelativeLayout>

Step 10. Go to the res folder create a new directory named menu and create a new Menu Resource file named menu.xml.

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_switch_layout"
        android:title="Change Layout"
        android:icon="@android:drawable/ic_lock_power_off"
        app:showAsAction="always" />
</menu>

Step 11. Now create a preference data store class named SettingDataStore.kt.

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"

// Create a DataStore instance using the preferencesDataStore delegate, with the Context as receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = LAYOUT_PREFERENCES_NAME)

class SettingsDataStore(context: Context) {

    private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")
    
    suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
        context.dataStore.edit { preferences ->
            preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
        }
    }

    val preferenceFlow: Flow<Boolean> = context.dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }
        .map { preferences ->
            // On the first run of the app, we will use LinearLayoutManager by default
            preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
        }

}

Here we have used Jetpack's DataStore to manage and store a boolean preference for the layout manager choice in an Android app. It provides functions to save layout preferences and exposes a Flow for observing changes in these preferences. 

Step 12. Now Create a new class named LetterAdapter.kt.

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class LetterAdapter(var context: Context, private val alphabetList: MutableList<Char>) : RecyclerView.Adapter<LetterAdapter.MyViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        var itemView=LayoutInflater.from(parent.context).inflate(R.layout.alphabet_item_layout, parent, false)
        return  MyViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.alphabetTextView.text=alphabetList.get(position).toString()
    }

    override fun getItemCount(): Int {
        return  alphabetList.size
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
        val alphabetTextView: TextView = itemView.findViewById(R.id.alphabet_textview)
    }

}

This adapter class takes a list of alphabet letters, inflates a custom layout for each item, and binds data to the views using a view holder.

Step 13. Now Edit your LetterListFragment.kt.

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch

class LetterListFragment : Fragment() {

    lateinit var alphabetRecylerView: RecyclerView
    val alphabetList= mutableListOf<Char>()
    private var isLinearLayoutManager=true
    private lateinit var settingsDataStore: SettingsDataStore

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        var view = inflater.inflate(R.layout.fragment_letter_list, container, false)
        return view
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        alphabetList.clear()

        settingsDataStore = SettingsDataStore(requireContext())

        for( i in 'A'..'Z'){
            alphabetList.add(i)
        }

        alphabetRecylerView=view.findViewById(R.id.alphabet_recylerview)

        settingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
            isLinearLayoutManager = value
            chooseLayout()
        })

    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.menu,menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_switch_layout -> {
                // Launch a coroutine and write the layout setting in the preference Datastore
                lifecycleScope.launch {
                    settingsDataStore.saveLayoutToPreferencesStore(!isLinearLayoutManager, requireContext())
                }
                return true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    private fun chooseLayout() {
        if (isLinearLayoutManager) {
            alphabetRecylerView.layoutManager = LinearLayoutManager(requireContext())
        } else {
            alphabetRecylerView.layoutManager = GridLayoutManager(requireContext(), 4)
        }
        alphabetRecylerView.adapter = LetterAdapter(requireContext(),alphabetList)
    }

}

This Fragment is used for displaying an alphabet list. It dynamically adjusts the layout of the RecyclerView based on user preferences stored in a SettingsDataStore. The fragment also includes functionality to switch between linear and grid layouts via a menu item.

Output

 

Conclusion

In this article, we have seen how to use the preferences datastore in Android. Thanks for reading, and hope you like it. If you have any suggestions or queries about this article, please share your thoughts. You can read my other articles by clicking here.

Resource

Happy learning, friends!


Similar Articles