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